Compare commits

...
29 Commits
Author SHA1 Message Date
SMKRV 5037622fb6 Release v2.0.0 2024-11-20 01:02:27 +03:00
SMKRV 962a089bf4 Stability improvements 2024-11-19 23:38:07 +03:00
SMKRV 4c56565b66 Stability improvements 2024-11-19 23:30:28 +03:00
SMKRV 8432038f09 Stability improvements 2024-11-19 23:21:54 +03:00
SMKRV 929d916d41 Bugfixes 2024-11-19 19:39:05 +03:00
SMKRV 675975d951 Minor changes 2024-11-19 19:31:26 +03:00
SMKRV 524ec87395 Minor changes 2024-11-19 19:30:42 +03:00
SMKRV 94f8193996 last_update_success_time > last_update_success 2024-11-19 19:28:01 +03:00
SMKRV 7c3fcf73c3 Text edits 2024-11-19 19:23:03 +03:00
SMKRV 20bbf89679 Changes 2024-11-19 19:20:52 +03:00
SMKRV 86dca52d07 Small changes 2024-11-19 19:18:16 +03:00
SMKRV 78d1561e74 Markdown 2024-11-19 19:17:04 +03:00
SMKRV 054e4af258 Quick bugfix 2024-11-19 19:15:57 +03:00
SMKRV dc03faa97e docs: update HACS installation for custom
repository

  - Change HACS badge from Default to Custom
  - Add custom repository installation steps
  - Update installation instructions
  - Revise documentation format
2024-11-19 19:03:51 +03:00
SMKRV 9a7635c2ae Release v1.1.0 2024-11-19 18:55:14 +03:00
SMKRV df3d79c20c feat: add multi-provider support,
config improvements
2024-11-19 18:53:51 +03:00
SMKRV d6144be7ed v.1.0.10 2024-11-19 17:49:48 +03:00
SMKRV 9afbb904b3 __init__.py:
added: from .coordinator import HATextAICoordinator
2024-11-19 17:48:00 +03:00
SMKRV 93558b2444 Version update 2024-11-19 17:34:27 +03:00
SMKRV 9f93f1ee18 Main changes:
Removed the validate_endpoint function
Optimized the validate_api_connection function
Simplified API connection verification
Preserved all error handling and retry logic
Improved exception handling
The integration should now correctly verify the
OpenAI API connection without false endpoint_not_available errors.
2024-11-19 17:33:27 +03:00
SMKRV 30a9b53ba1 Markdown changes 2024-11-19 17:18:23 +03:00
SMKRV 5175970d55 structure.md added 2024-11-19 17:16:30 +03:00
SMKRV f6bfbd4a07 Release v1.0.8 2024-11-19 17:02:38 +03:00
SMKRV 4ddb0dc977 Translation fixes 2024-11-19 16:59:42 +03:00
SMKRV 24dc4ac4d4 Hotfix 2024-11-19 16:53:27 +03:00
SMKRV 976f4f16a3 translation fixes 2024-11-19 16:40:56 +03:00
SMKRV 2bdef2b494 Main changes:
Created global SSL_CONTEXT at module level
Removed blocking create_default_context calls from async functions
Optimized aiohttp.ClientSession handling:
Using single connector with SSL context
Session is created once for all requests in validate_api_connection
Improved resource management:
Automatic session closure using context managers
More efficient connection handling
These changes should eliminate the blocking call warning and improve
overall code performance.
2024-11-19 16:33:30 +03:00
SMKRV 42324a793b bufix 2024-11-19 16:25:44 +03:00
SMKRV 30aa894634 Release v1.0.6 2024-11-19 15:10:06 +03:00
13 changed files with 1367 additions and 771 deletions
+63 -128
View File
@@ -1,73 +1,81 @@
# 🤖 HA Text AI for Home Assistant # 🤖 HA Text AI for Home Assistant
<div align="center"> <div align="center">
![GitHub release](https://img.shields.io/github/release/smkrv/ha-text-ai.svg?style=flat-square) ![GitHub release](https://img.shields.io/github/release/smkrv/ha-text-ai.svg?style=flat-square) ![GitHub downloads](https://img.shields.io/github/downloads/smkrv/ha-text-ai/total.svg?style=flat-square) ![GitHub stars](https://img.shields.io/github/stars/smkrv/ha-text-ai.svg?style=social) ![GitHub last commit](https://img.shields.io/github/last-commit/smkrv/ha-text-ai.svg?style=flat-square) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT) [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=flat-square)](https://github.com/hacs/integration)
![GitHub downloads](https://img.shields.io/github/downloads/smkrv/ha-text-ai/total.svg?style=flat-square)
![GitHub stars](https://img.shields.io/github/stars/smkrv/ha-text-ai.svg?style=social)
![GitHub last commit](https://img.shields.io/github/last-commit/smkrv/ha-text-ai.svg?style=flat-square)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=flat-square)](https://github.com/hacs/integration)
[![Community Forum](https://img.shields.io/badge/Community-Forum-blue.svg?style=flat-square)](https://community.home-assistant.io/t/ha-text-ai-integration)
</div> </div>
<p align="center"> <p align="center">
Transform your smart home experience with powerful AI assistance powered by OpenAI's GPT models. Get intelligent responses, automate complex scenarios, and enhance your home automation with natural language processing. Transform your smart home experience with powerful AI assistance powered by multiple AI providers including OpenAI GPT and Anthropic Claude models. Get intelligent responses, automate complex scenarios, and enhance your home automation with advanced natural language processing.
</p> </p>
--- ---
## 🌟 Features ## 🌟 Features
- 🧠 **Advanced AI Integration**: - 🧠 **Multi-Provider AI Integration**:
- Support for latest GPT models - Support for OpenAI GPT models
- Anthropic Claude integration
- Custom API endpoints
- Flexible model selection
- 💬 **Advanced Language Processing**:
- Context-aware responses - Context-aware responses
- Multi-turn conversations - Multi-turn conversations
- 💬 **Natural Language Control**: - Custom system instructions
- Control devices using everyday language
- Get detailed explanations and recommendations
- Natural conversation flow - Natural conversation flow
- 📝 **Smart Memory Management**: - 📝 **Enhanced Memory Management**:
- Persistent conversation history - Persistent conversation history
- Context-aware responses - Context-aware responses
- Customizable history limits - Customizable history limits
-**Performance Optimized**: - Model-specific filtering
-**Performance Optimization**:
- Efficient token usage - Efficient token usage
- Rate limit handling - Smart rate limiting
- Response caching - Response caching
- Request interval control
- 🎯 **Advanced Customization**: - 🎯 **Advanced Customization**:
- Adjustable response parameters - Per-request model selection
- Adjustable parameters
- Custom system prompts - Custom system prompts
- Model selection per request - Temperature control
- 🔒 **Enhanced Security**: - 🔒 **Enhanced Security**:
- Secure API key storage - Secure API key storage
- Rate limiting protection - Rate limiting protection
- Error handling - Error handling
- 🎨 **User Experience**: - Usage monitoring
- 🎨 **Improved User Experience**:
- Intuitive configuration UI - Intuitive configuration UI
- Detailed sensor attributes - Detailed sensor attributes
- Rich service interface - Rich service interface
- Model selection UI
- 🔄 **Automation Integration**: - 🔄 **Automation Integration**:
- Event-driven responses - Event-driven responses
- Conditional logic support - Conditional logic support
- Template compatibility - Template compatibility
- Model-specific automation
## 📋 Prerequisites ## 📋 Prerequisites
- Home Assistant 2023.8.0 or newer - Home Assistant 2023.8.0 or newer
- OpenAI API key ([Get one here](https://platform.openai.com/account/api-keys)) - API key from supported providers:
- OpenAI ([Get key](https://platform.openai.com/account/api-keys))
- Anthropic ([Get key](https://console.anthropic.com/))
- Python 3.9 or newer - Python 3.9 or newer
- Stable internet connection - Stable internet connection
## ⚡ Installation ## ⚡ Installation
### HACS Installation (Recommended) ### HACS Installation (Recommended)
1. Open HACS in Home Assistant 1. Open HACS in Home Assistant
2. Click the "+" button 2. Click on the three dots in the top right corner
3. Search for "HA Text AI" 3. Select "Custom repositories"
4. Click "Install" 4. Add `https://github.com/smkrv/ha-text-ai` as Integration
5. Restart Home Assistant 5. Click "Add"
6. Click on "Integrations" in HACS
7. Search for "HA Text AI"
8. Click "Download"
9. Restart Home Assistant
### Manual Installation ### Manual Installation
1. Download the latest release 1. Download the latest release
@@ -86,12 +94,15 @@ Transform your smart home experience with powerful AI assistance powered by Open
### Via YAML ### Via YAML
```yaml ```yaml
ha_text_ai: ha_text_ai:
api_key: !secret openai_api_key api_key: !secret ai_api_key
model: gpt-3.5-turbo model: gpt-3.5-turbo # or claude-3-sonnet
temperature: 0.7 temperature: 0.7
max_tokens: 1000 max_tokens: 1000
request_interval: 1.0 request_interval: 1.0
api_endpoint: https://api.openai.com/v1 # optional api_endpoint: https://api.openai.com/v1 # optional, for custom endpoints
system_prompt: |
You are a home automation expert assistant.
Focus on practical and efficient solutions.
``` ```
## 🛠️ Available Services ## 🛠️ Available Services
@@ -101,9 +112,10 @@ ha_text_ai:
service: ha_text_ai.ask_question service: ha_text_ai.ask_question
data: data:
question: "What's the optimal temperature for sleeping?" question: "What's the optimal temperature for sleeping?"
model: "gpt-4o" # optional model: "claude-3-sonnet" # optional
temperature: 0.5 # optional temperature: 0.5 # optional
max_tokens: 500 # optional max_tokens: 500 # optional
system_prompt: "You are a sleep optimization expert" # optional
``` ```
### set_system_prompt ### set_system_prompt
@@ -128,114 +140,37 @@ service: ha_text_ai.clear_history
service: ha_text_ai.get_history service: ha_text_ai.get_history
data: data:
limit: 5 # optional limit: 5 # optional
filter_model: "gpt-4" # optional
``` ```
## 🔧 Advanced Examples
### Smart Energy Management
```yaml
automation:
alias: "AI Energy Optimization"
trigger:
platform: time_pattern
hours: "/2"
action:
- service: ha_text_ai.ask_question
data:
question: >
Current power usage: {{ states('sensor.total_power') }}W
Temperature: {{ states('sensor.indoor_temperature') }}°C
Time: {{ now().strftime('%H:%M') }}
Occupancy: {{ states('binary_sensor.occupancy') }}
Analyze current energy usage and suggest optimizations
considering comfort and efficiency.
temperature: 0.3
max_tokens: 200
- service: notify.mobile_app
data:
message: "{{ states.sensor.ha_text_ai.attributes.response }}"
```
### Contextual Lighting Control
```yaml
automation:
alias: "AI Lighting Assistant"
trigger:
platform: state
entity_id: binary_sensor.motion
variables:
context: >
Time: {{ now().strftime('%H:%M') }}
Light Level: {{ states('sensor.illuminance') }}
Room: {{ trigger.to_state.attributes.room }}
Activity: {{ states('input_select.current_activity') }}
Weather: {{ states('weather.home') }}
action:
- service: ha_text_ai.ask_question
data:
question: >
Based on this context:
{{ context }}
Suggest optimal lighting settings for current conditions.
model: gpt-3.5-turbo
temperature: 0.4
- service: scene.turn_on
data:
entity_id: >
{{ states.sensor.ha_text_ai.attributes.response | regex_findall('scene\.[a-z_]+') | first }}
```
## 📊 Performance Optimization
### Token Usage
- Use focused system prompts
- Implement response caching
- Clear history periodically
- Monitor token usage
### Response Time
- Adjust request_interval
- Use faster models for simple queries
- Implement timeout handling
- Cache frequent responses
### Memory Management
- Set appropriate history limits
- Clear unused contexts
- Monitor memory usage
- Use efficient data structures
## ❗ Troubleshooting
### API Issues
- Verify API key validity
- Check rate limits
- Monitor usage quotas
- Test endpoint accessibility
### Performance Issues
- Reduce max_tokens
- Increase request_interval
- Clear conversation history
- Check network connectivity
### Integration Issues
- Verify HA version compatibility
- Check component dependencies
- Review log files
- Update configuration
## 📘 FAQ ## 📘 FAQ
**Q: Which AI providers are supported?**
A: Currently OpenAI (GPT models) and Anthropic (Claude models) are supported, with more providers planned.
**Q: How can I reduce API costs?** **Q: How can I reduce API costs?**
A: Use GPT-3.5-Turbo or Claude-3-Sonnet for most queries, implement caching, and optimize token usage.
**Q: Can I use custom models?**
A: Yes, you can configure custom endpoints and use any compatible model by specifying it in the configuration.
**Q: How do I switch between different AI providers?**
A: Simply change the model parameter in your configuration or service calls to use the desired provider's model.
**Q: How can I reduce API costs?**
A: Use GPT-3.5-Turbo for most queries, implement caching, and optimize token usage. A: Use GPT-3.5-Turbo for most queries, implement caching, and optimize token usage.
**Q: Is my data secure?** **Q: Is my data secure?**
A: Yes, API keys are stored securely and data is transmitted via encrypted connections. A: Yes, API keys are stored securely and data is transmitted via encrypted connections.
**Q: Can I use custom models?** **Q: Can I use custom models?**
A: Yes, configure custom endpoints and models via configuration options. A: Yes, configure custom endpoints and models via configuration options.
## 🤝 Contributing ## 🤝 Contributing
+325 -39
View File
@@ -1,21 +1,121 @@
"""The HA Text AI integration.""" """The HA Text AI integration."""
import logging import logging
from typing import Any from typing import Any, Dict, Optional
import asyncio
import voluptuous as vol
import json
from datetime import datetime
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.helpers import config_validation as cv
from async_timeout import timeout
from .const import DOMAIN, PLATFORMS
from .coordinator import HATextAICoordinator from .coordinator import HATextAICoordinator
from .const import (
DOMAIN,
PLATFORMS,
CONF_MODEL,
CONF_TEMPERATURE,
CONF_MAX_TOKENS,
CONF_API_ENDPOINT,
CONF_REQUEST_INTERVAL,
DEFAULT_MODEL,
DEFAULT_TEMPERATURE,
DEFAULT_MAX_TOKENS,
DEFAULT_API_ENDPOINT,
DEFAULT_REQUEST_INTERVAL,
API_VERSION,
API_MODELS_PATH,
API_CHAT_PATH,
API_TIMEOUT,
API_RETRY_COUNT,
API_BACKOFF_FACTOR,
LOGGER_NAME,
STATE_ERROR,
STATE_READY,
STATE_PROCESSING,
STATE_RATE_LIMITED,
STATE_MAINTENANCE,
STATE_DISCONNECTED,
STATE_RETRYING,
STATE_QUEUED,
STATE_UPDATING,
SUPPORTED_MODELS,
EVENT_RESPONSE_RECEIVED,
EVENT_ERROR_OCCURRED,
EVENT_STATE_CHANGED,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(LOGGER_NAME)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
# Service validation schemas
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
vol.Required("question"): cv.string,
vol.Optional("system_prompt"): cv.string,
vol.Optional("model"): vol.In(SUPPORTED_MODELS),
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)
),
vol.Optional("priority"): vol.Boolean,
})
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
vol.Optional("limit", default=10): vol.All(
vol.Coerce(int), vol.Range(min=1, max=100)
),
vol.Optional("filter_model"): vol.In(SUPPORTED_MODELS),
vol.Optional("start_date"): cv.datetime,
vol.Optional("include_metadata"): vol.Boolean,
})
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
vol.Required("prompt"): cv.string,
})
async def async_check_api(session, endpoint: str, headers: dict, is_anthropic: bool = False) -> bool:
"""Check API availability for different providers."""
try:
if is_anthropic:
check_url = f"{endpoint}/v1/models"
else:
check_url = f"{endpoint}/{API_VERSION}/{API_MODELS_PATH}"
async with timeout(API_TIMEOUT):
async with session.get(check_url, headers=headers) as response:
if response.status == 200:
return True
elif response.status == 401:
raise ConfigEntryNotReady("Invalid API key")
elif response.status == 429:
_LOGGER.warning("Rate limit exceeded during API check")
return False
else:
_LOGGER.error("API check failed with status: %d", response.status)
return False
except Exception as ex:
_LOGGER.error("API check error: %s", str(ex))
return False
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up the HA Text AI component.""" """Set up the HA Text AI component."""
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {
"coordinators": {},
"metrics": {
"total_requests": 0,
"total_tokens": 0,
"errors": {},
"model_usage": {},
}
})
return True return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -23,56 +123,234 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try: try:
session = aiohttp_client.async_get_clientsession(hass) session = aiohttp_client.async_get_clientsession(hass)
# Determine API type based on model
model = entry.data.get(CONF_MODEL, DEFAULT_MODEL)
is_anthropic = any(model.startswith("claude") for model in SUPPORTED_MODELS)
api_key = entry.data[CONF_API_KEY]
endpoint = entry.data.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT).rstrip('/')
# Configure headers based on API type
headers = {
"Content-Type": "application/json",
"Accept": "application/json"
}
if is_anthropic:
headers["x-api-key"] = api_key
headers["anthropic-version"] = "2023-06-01"
else:
headers["Authorization"] = f"Bearer {api_key}"
# Check API with retries
for attempt in range(API_RETRY_COUNT):
if await async_check_api(session, endpoint, headers, is_anthropic):
break
if attempt < API_RETRY_COUNT - 1:
delay = API_BACKOFF_FACTOR * (2 ** attempt)
await asyncio.sleep(delay)
else:
raise ConfigEntryNotReady("Failed to connect to API")
# Create and initialize coordinator
coordinator = HATextAICoordinator( coordinator = HATextAICoordinator(
hass, hass,
api_key=entry.data[CONF_API_KEY], api_key=api_key,
endpoint=entry.data.get("api_endpoint", "https://api.openai.com/v1"), endpoint=endpoint,
model=entry.data.get("model", "gpt-3.5-turbo"), model=model,
temperature=entry.data.get("temperature", 0.7), temperature=entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
max_tokens=entry.data.get("max_tokens", 1000), max_tokens=entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
request_interval=entry.data.get("request_interval", 1.0), request_interval=entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
session=session, session=session,
is_anthropic=is_anthropic
) )
try: # Initialize coordinator
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
except Exception as refresh_ex:
_LOGGER.error("Failed to refresh coordinator: %s", str(refresh_ex))
return False
if not coordinator.last_update_success: # Check coordinator status
_LOGGER.error("Failed to communicate with OpenAI API") if coordinator.endpoint_status == "auth_error":
return False raise ConfigEntryNotReady("Authentication failed")
elif coordinator.endpoint_status == "rate_limited":
_LOGGER.warning("API rate limited during setup")
elif coordinator.endpoint_status == "maintenance":
raise ConfigEntryNotReady("API is in maintenance mode")
elif coordinator.endpoint_status == "error":
raise ConfigEntryNotReady("API error during setup")
elif not coordinator.last_update_success:
raise ConfigEntryNotReady("Failed to initialize coordinator")
hass.data[DOMAIN][entry.entry_id] = coordinator hass.data[DOMAIN][entry.entry_id] = coordinator
try: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
except Exception as setup_ex: # Register event handlers
_LOGGER.error("Failed to setup platforms: %s", str(setup_ex)) @callback
return False def handle_state_change(event):
"""Handle state changes."""
if event.data.get("entity_id").startswith(f"{DOMAIN}."):
_LOGGER.debug("State changed: %s", event.data)
hass.bus.async_listen(EVENT_STATE_CHANGED, handle_state_change)
# Register services
async def async_ask_question(call: ServiceCall) -> None:
"""Handle the ask_question service call."""
question = call.data.get("question", "")
if not question:
_LOGGER.error("No question provided in service call")
return
request_params = {}
for param in ["system_prompt", "model", "temperature", "max_tokens"]:
if param in call.data:
request_params[param] = call.data[param]
try:
await coordinator.async_ask_question(question, **request_params)
except Exception as err:
_LOGGER.error("Error asking question: %s", str(err))
async def async_clear_history(call: ServiceCall) -> None:
"""Handle the clear_history service call."""
try:
coordinator._responses.clear()
await coordinator.async_refresh()
_LOGGER.info("History cleared successfully")
except Exception as err:
_LOGGER.error("Error clearing history: %s", str(err))
async def async_get_history(call: ServiceCall) -> dict:
"""Handle the get_history service call."""
try:
limit = min(int(call.data.get("limit", 10)), 100)
filter_model = str(call.data.get("filter_model", ""))
start_date = call.data.get("start_date")
include_metadata = call.data.get("include_metadata", False)
responses = coordinator._responses
metrics = {
"total_requests": coordinator.request_count,
"total_tokens": coordinator.tokens_used,
"api_version": coordinator.api_version,
"endpoint_status": coordinator.endpoint_status,
"error_count": coordinator.error_count
}
filtered_responses = responses.copy()
if filter_model:
filtered_responses = {
k: v for k, v in filtered_responses.items()
if v.get("model") == filter_model
}
if start_date:
filtered_responses = {
k: v for k, v in filtered_responses.items()
if v.get("timestamp") >= start_date
}
if not include_metadata:
filtered_responses = {
k: {
"question": v["question"],
"response": v["response"],
"timestamp": v["timestamp"]
} for k, v in filtered_responses.items()
}
sorted_responses = dict(
sorted(
filtered_responses.items(),
key=lambda x: x[1]["timestamp"],
reverse=True
)[:limit]
)
return {
"metrics": metrics,
"responses": sorted_responses
}
except Exception as err:
_LOGGER.error("Error getting history: %s", str(err))
return {}
async def async_set_system_prompt(call: ServiceCall) -> None:
"""Handle the set_system_prompt service call."""
try:
prompt = str(call.data.get("prompt", "")).strip()
if prompt:
coordinator.system_prompt = prompt
_LOGGER.info("System prompt updated successfully")
else:
_LOGGER.error("Empty prompt provided")
except Exception as err:
_LOGGER.error("Error setting system prompt: %s", str(err))
# Register services with validation
hass.services.async_register(
DOMAIN,
"ask_question",
async_ask_question,
schema=SERVICE_SCHEMA_ASK_QUESTION
)
hass.services.async_register(
DOMAIN,
"clear_history",
async_clear_history
)
hass.services.async_register(
DOMAIN,
"get_history",
async_get_history,
schema=SERVICE_SCHEMA_GET_HISTORY
)
hass.services.async_register(
DOMAIN,
"set_system_prompt",
async_set_system_prompt,
schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT
)
_LOGGER.info( _LOGGER.info(
"Successfully set up HA Text AI with model: %s", "Successfully set up HA Text AI with model: %s",
entry.data.get("model", "gpt-3.5-turbo") entry.data.get(CONF_MODEL, DEFAULT_MODEL)
) )
return True return True
except Exception as ex: except Exception as ex:
_LOGGER.exception("Unexpected error setting up entry: %s", str(ex)) _LOGGER.exception("Setup error: %s", str(ex))
return False raise ConfigEntryNotReady from ex
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
try: try:
if entry.entry_id not in hass.data.get(DOMAIN, {}): coordinator = hass.data[DOMAIN].get(entry.entry_id)
return True if coordinator:
# Clear queue and history
coordinator._responses.clear()
while not coordinator._question_queue.empty():
try:
coordinator._question_queue.get_nowait()
coordinator._question_queue.task_done()
except Exception:
pass
# Close connection
await coordinator.async_shutdown()
# Remove services
for service in ["ask_question", "clear_history", "get_history", "set_system_prompt"]:
hass.services.async_remove(DOMAIN, service)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok: if unload_ok:
coordinator = hass.data[DOMAIN].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)
await coordinator.async_shutdown()
return unload_ok return unload_ok
@@ -80,10 +358,18 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.exception("Error unloading entry: %s", str(ex)) _LOGGER.exception("Error unloading entry: %s", str(ex))
return False return False
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Reload config entry.""" """Migrate old entry."""
try: _LOGGER.debug("Migrating from version %s", entry.version)
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry) if entry.version == 1:
except Exception as ex: new = {**entry.data}
_LOGGER.exception("Error reloading entry: %s", str(ex))
# Migrate settings
if CONF_MODEL in new and new[CONF_MODEL] not in SUPPORTED_MODELS:
new[CONF_MODEL] = DEFAULT_MODEL
entry.version = 2
hass.config_entries.async_update_entry(entry, data=new)
return True
+136 -125
View File
@@ -1,19 +1,17 @@
"""Config flow for HA text AI integration.""" """Config flow for HA text AI integration."""
from typing import Any, Dict, Optional, Tuple from typing import Any, Dict, Optional, Tuple
import voluptuous as vol import voluptuous as vol
import ssl
import certifi
import asyncio import asyncio
from async_timeout import timeout
import aiohttp import aiohttp
from urllib.parse import urlparse from async_timeout import timeout
from urllib.parse import urlparse, urljoin
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.core import callback from homeassistant.core import callback
from openai import AsyncOpenAI from homeassistant.data_entry_flow import FlowResult
from openai import OpenAIError, APIError, APIConnectionError, AuthenticationError, RateLimitError from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ( from .const import (
DOMAIN, DOMAIN,
@@ -27,106 +25,116 @@ from .const import (
DEFAULT_MAX_TOKENS, DEFAULT_MAX_TOKENS,
DEFAULT_API_ENDPOINT, DEFAULT_API_ENDPOINT,
DEFAULT_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL,
MIN_TEMPERATURE,
MAX_TEMPERATURE,
MIN_MAX_TOKENS,
MAX_MAX_TOKENS,
MIN_REQUEST_INTERVAL,
API_VERSION,
API_MODELS_PATH,
ERROR_INVALID_API_KEY,
ERROR_CANNOT_CONNECT,
ERROR_UNKNOWN,
ERROR_INVALID_MODEL,
ERROR_RATE_LIMIT,
ERROR_API_ERROR,
ERROR_TIMEOUT,
) )
import logging import logging
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema({ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_API_KEY): str, {
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): str, vol.Required(CONF_API_KEY): cv.string,
vol.Optional( vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): cv.string,
CONF_TEMPERATURE, vol.Optional(
default=DEFAULT_TEMPERATURE CONF_TEMPERATURE,
): vol.All( default=DEFAULT_TEMPERATURE
vol.Coerce(float), ): vol.All(
vol.Range(min=0, max=2), vol.Coerce(float),
msg="Temperature must be between 0 and 2" vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
), ),
vol.Optional( vol.Optional(
CONF_MAX_TOKENS, CONF_MAX_TOKENS,
default=DEFAULT_MAX_TOKENS default=DEFAULT_MAX_TOKENS
): vol.All( ): vol.All(
vol.Coerce(int), vol.Coerce(int),
vol.Range(min=1, max=4096), vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
msg="Max tokens must be between 1 and 4096" ),
), vol.Optional(
vol.Optional(CONF_API_ENDPOINT, default=DEFAULT_API_ENDPOINT): vol.All( CONF_API_ENDPOINT,
str, default=DEFAULT_API_ENDPOINT
vol.Url(), # Заменено с URL на Url ): cv.string,
msg="Must be a valid URL" vol.Optional(
), CONF_REQUEST_INTERVAL,
vol.Optional( default=DEFAULT_REQUEST_INTERVAL
CONF_REQUEST_INTERVAL, ): vol.All(
default=DEFAULT_REQUEST_INTERVAL vol.Coerce(float),
): vol.All( vol.Range(min=MIN_REQUEST_INTERVAL)
vol.Coerce(float), ),
vol.Range(min=0.1), }
msg="Request interval must be at least 0.1 seconds" )
),
})
async def validate_endpoint(endpoint: str) -> Tuple[bool, str]:
"""Validate API endpoint accessibility."""
try:
parsed_url = urlparse(endpoint)
if parsed_url.scheme not in ('http', 'https'):
return False, "invalid_endpoint_scheme"
ssl_context = ssl.create_default_context(cafile=certifi.where())
async with timeout(5):
async with aiohttp.ClientSession() as session:
async with session.get(endpoint, ssl=ssl_context) as response:
if response.status != 200:
return False, "endpoint_not_available"
return True, ""
except Exception as e:
_LOGGER.error("Error validating endpoint: %s", str(e))
return False, "endpoint_error"
async def validate_api_connection( async def validate_api_connection(
hass,
api_key: str, api_key: str,
endpoint: str, endpoint: str,
model: str, model: str,
retry_count: int = 3, retry_count: int = 3,
retry_delay: float = 1.0 retry_delay: float = 1.0
) -> Tuple[bool, str, list]: ) -> Tuple[bool, str, list]:
"""Validate API connection with improved retry logic.""" """Validate API connection with retry logic."""
ssl_context = ssl.create_default_context(cafile=certifi.where()) session = async_get_clientsession(hass)
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json; charset=utf-8",
"Accept": "application/json; charset=utf-8",
"Accept-Charset": "utf-8"
}
# Validate endpoint first base_url = endpoint.rstrip('/')
endpoint_valid, endpoint_error = await validate_endpoint(endpoint) if not base_url.endswith(f"/{API_VERSION}"):
if not endpoint_valid: base_url = f"{base_url}/{API_VERSION}"
return False, endpoint_error, []
models_url = f"{base_url}/{API_MODELS_PATH}"
_LOGGER.debug("Attempting to connect to: %s", models_url)
for attempt in range(retry_count): for attempt in range(retry_count):
try: try:
async with timeout(10): async with timeout(10):
client = AsyncOpenAI( async with session.get(models_url, headers=headers) as response:
api_key=api_key, if response.status == 200:
base_url=endpoint, data = await response.json()
http_client=aiohttp.ClientSession( model_ids = [m["id"] for m in data.get("data", [])]
connector=aiohttp.TCPConnector(
ssl=ssl_context, _LOGGER.debug("Available models: %s", ", ".join(model_ids))
enable_cleanup_closed=True
if model not in model_ids:
_LOGGER.warning(
"Model %s not found in available models: %s",
model,
", ".join(model_ids)
)
return False, ERROR_INVALID_MODEL, model_ids
return True, "", model_ids
elif response.status == 401:
_LOGGER.error("Authentication failed")
return False, ERROR_INVALID_API_KEY, []
elif response.status == 429:
_LOGGER.error("Rate limit exceeded")
return False, ERROR_RATE_LIMIT, []
else:
response_text = await response.text()
_LOGGER.error(
"API error: %s - %s",
response.status,
response_text
) )
) return False, ERROR_API_ERROR, []
)
try:
models = await client.models.list()
model_ids = [model.id for model in models.data]
finally:
await client.http_client.close()
if model not in model_ids:
_LOGGER.warning(
"Model %s not found in available models: %s",
model,
", ".join(model_ids)
)
return False, "invalid_model", model_ids
return True, "", model_ids
except asyncio.TimeoutError: except asyncio.TimeoutError:
_LOGGER.warning( _LOGGER.warning(
@@ -135,28 +143,18 @@ async def validate_api_connection(
retry_count retry_count
) )
if attempt == retry_count - 1: if attempt == retry_count - 1:
return False, "timeout", [] return False, ERROR_TIMEOUT, []
await asyncio.sleep(retry_delay) await asyncio.sleep(retry_delay)
except AuthenticationError as err: except aiohttp.ClientError as err:
_LOGGER.error("Authentication error: %s", str(err)) _LOGGER.error("Connection error: %s", str(err))
return False, "invalid_auth", [] return False, ERROR_CANNOT_CONNECT, []
except RateLimitError as err:
_LOGGER.error("Rate limit exceeded: %s", str(err))
return False, "rate_limit", []
except APIConnectionError as err:
_LOGGER.error("API connection error: %s", str(err))
return False, "cannot_connect", []
except APIError as err:
_LOGGER.error("API error: %s", str(err))
return False, "api_error", []
except Exception as err: except Exception as err:
_LOGGER.exception("Unexpected error during validation: %s", str(err)) _LOGGER.exception("Unexpected error during validation: %s", str(err))
return False, "unknown", [] return False, ERROR_UNKNOWN, []
return False, ERROR_UNKNOWN, []
class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for HA text AI.""" """Handle a config flow for HA text AI."""
@@ -166,35 +164,54 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_user( async def async_step_user(
self, self,
user_input: Optional[Dict[str, Any]] = None user_input: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]: ) -> FlowResult:
"""Handle the initial step.""" """Handle the initial step."""
errors: Dict[str, str] = {} errors: Dict[str, str] = {}
if user_input is not None: if user_input is not None:
try: try:
# Validate input data endpoint = user_input.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT)
user_input = STEP_USER_DATA_SCHEMA(user_input) try:
result = urlparse(endpoint)
if not all([result.scheme, result.netloc]):
errors["base"] = "invalid_url_format"
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors
)
except Exception as e:
_LOGGER.error("URL parsing error: %s", str(e))
errors["base"] = "invalid_url_format"
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors
)
validated_input = STEP_USER_DATA_SCHEMA(user_input)
is_valid, error_code, available_models = await validate_api_connection( is_valid, error_code, available_models = await validate_api_connection(
user_input[CONF_API_KEY], self.hass,
user_input.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT), validated_input[CONF_API_KEY],
user_input[CONF_MODEL] endpoint,
validated_input[CONF_MODEL]
) )
if is_valid: if is_valid:
await self.async_set_unique_id(user_input[CONF_API_KEY]) await self.async_set_unique_id(validated_input[CONF_API_KEY])
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry( return self.async_create_entry(
title="HA text AI", title="HA Text AI",
data=user_input data=validated_input
) )
errors["base"] = error_code errors["base"] = error_code
if error_code == "invalid_model": if error_code == ERROR_INVALID_MODEL:
_LOGGER.warning( _LOGGER.warning(
"Selected model %s not found in available models: %s", "Selected model %s not found in available models: %s",
user_input[CONF_MODEL], validated_input[CONF_MODEL],
", ".join(available_models) ", ".join(available_models)
) )
@@ -230,7 +247,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_init( async def async_step_init(
self, self,
user_input: Optional[Dict[str, Any]] = None user_input: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]: ) -> FlowResult:
"""Handle options flow.""" """Handle options flow."""
if user_input is not None: if user_input is not None:
return self.async_create_entry(title="", data=user_input) return self.async_create_entry(title="", data=user_input)
@@ -241,33 +258,27 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
default=self.config_entry.options.get( default=self.config_entry.options.get(
CONF_TEMPERATURE, DEFAULT_TEMPERATURE CONF_TEMPERATURE, DEFAULT_TEMPERATURE
), ),
description={"suggested_value": DEFAULT_TEMPERATURE},
): vol.All( ): vol.All(
vol.Coerce(float), vol.Coerce(float),
vol.Range(min=0, max=2), vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
msg="Temperature must be between 0 and 2"
), ),
vol.Optional( vol.Optional(
CONF_MAX_TOKENS, CONF_MAX_TOKENS,
default=self.config_entry.options.get( default=self.config_entry.options.get(
CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS
), ),
description={"suggested_value": DEFAULT_MAX_TOKENS},
): vol.All( ): vol.All(
vol.Coerce(int), vol.Coerce(int),
vol.Range(min=1, max=4096), vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
msg="Max tokens must be between 1 and 4096"
), ),
vol.Optional( vol.Optional(
CONF_REQUEST_INTERVAL, CONF_REQUEST_INTERVAL,
default=self.config_entry.options.get( default=self.config_entry.options.get(
CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL
), ),
description={"suggested_value": DEFAULT_REQUEST_INTERVAL},
): vol.All( ): vol.All(
vol.Coerce(float), vol.Coerce(float),
vol.Range(min=0.1), vol.Range(min=MIN_REQUEST_INTERVAL)
msg="Request interval must be at least 0.1 seconds"
), ),
}) })
+50 -7
View File
@@ -13,15 +13,27 @@ CONF_MAX_TOKENS: Final = "max_tokens"
CONF_API_ENDPOINT: Final = "api_endpoint" CONF_API_ENDPOINT: Final = "api_endpoint"
CONF_REQUEST_INTERVAL: Final = "request_interval" CONF_REQUEST_INTERVAL: Final = "request_interval"
# Model constants
SUPPORTED_MODELS: Final = [
"gpt-3.5-turbo",
"gpt-3.5-turbo-16k",
"gpt-4",
"gpt-4-32k",
"gpt-4-1106-preview",
"claude-3-sonnet",
"claude-3-opus"
]
# Default values # Default values
DEFAULT_MODEL: Final = "gpt-3.5-turbo" DEFAULT_MODEL: Final = "gpt-3.5-turbo"
DEFAULT_TEMPERATURE: Final = 0.7 DEFAULT_TEMPERATURE: Final = 0.1
DEFAULT_MAX_TOKENS: Final = 1000 DEFAULT_MAX_TOKENS: Final = 1000
DEFAULT_API_ENDPOINT: Final = "https://api.openai.com/v1" DEFAULT_API_ENDPOINT: Final = "https://api.openai.com"
DEFAULT_REQUEST_INTERVAL: Final = 1.0 DEFAULT_REQUEST_INTERVAL: Final = 1.0
DEFAULT_TIMEOUT: Final = 30 DEFAULT_TIMEOUT: Final = 30
DEFAULT_QUEUE_SIZE: Final = 100 DEFAULT_QUEUE_SIZE: Final = 100
DEFAULT_HISTORY_LIMIT: Final = 50 DEFAULT_HISTORY_LIMIT: Final = 50
DEFAULT_RETRY_COUNT: Final = 3
# Parameter constraints # Parameter constraints
MIN_TEMPERATURE: Final = 0.0 MIN_TEMPERATURE: Final = 0.0
@@ -31,6 +43,23 @@ MAX_MAX_TOKENS: Final = 4096
MIN_REQUEST_INTERVAL: Final = 0.1 MIN_REQUEST_INTERVAL: Final = 0.1
MIN_TIMEOUT: Final = 5 MIN_TIMEOUT: Final = 5
MAX_TIMEOUT: Final = 120 MAX_TIMEOUT: Final = 120
MAX_PROMPT_LENGTH: Final = 1000
MAX_HISTORY_LIMIT: Final = 100
# API constants
API_VERSION: Final = "v1"
API_MODELS_PATH: Final = "models"
API_CHAT_PATH: Final = "chat/completions"
API_TIMEOUT: Final = 30
API_RETRY_COUNT: Final = 3
API_BACKOFF_FACTOR: Final = 1.5
API_MAX_RETRIES: Final = 3
# History constants
HISTORY_FILTER_MODEL: Final = "filter_model"
HISTORY_FILTER_DATE: Final = "start_date"
HISTORY_SORT_ORDER: Final = "sort_order"
HISTORY_INCLUDE_METADATA: Final = "include_metadata"
# Service names # Service names
SERVICE_ASK_QUESTION: Final = "ask_question" SERVICE_ASK_QUESTION: Final = "ask_question"
@@ -58,6 +87,13 @@ ATTR_QUEUE_SIZE: Final = "queue_size"
ATTR_API_STATUS: Final = "api_status" ATTR_API_STATUS: Final = "api_status"
ATTR_ERROR_COUNT: Final = "error_count" ATTR_ERROR_COUNT: Final = "error_count"
ATTR_LAST_ERROR: Final = "last_error" ATTR_LAST_ERROR: Final = "last_error"
ATTR_API_VERSION: Final = "api_version"
ATTR_ENDPOINT_STATUS: Final = "endpoint_status"
ATTR_REQUEST_COUNT: Final = "request_count"
ATTR_TOKENS_USED: Final = "tokens_used"
ATTR_RETRY_COUNT: Final = "retry_count"
ATTR_QUEUE_POSITION: Final = "queue_position"
ATTR_ESTIMATED_WAIT: Final = "estimated_wait"
# Error messages # Error messages
ERROR_INVALID_API_KEY: Final = "invalid_api_key" ERROR_INVALID_API_KEY: Final = "invalid_api_key"
@@ -70,6 +106,8 @@ ERROR_API_ERROR: Final = "api_error"
ERROR_TIMEOUT: Final = "timeout_error" ERROR_TIMEOUT: Final = "timeout_error"
ERROR_QUEUE_FULL: Final = "queue_full" ERROR_QUEUE_FULL: Final = "queue_full"
ERROR_INVALID_PROMPT: Final = "invalid_prompt" ERROR_INVALID_PROMPT: Final = "invalid_prompt"
ERROR_INVALID_PARAMETERS: Final = "invalid_parameters"
ERROR_SERVICE_UNAVAILABLE: Final = "service_unavailable"
# Configuration descriptions # Configuration descriptions
CONF_MODEL_DESCRIPTION: Final = "AI model to use for responses" CONF_MODEL_DESCRIPTION: Final = "AI model to use for responses"
@@ -92,12 +130,18 @@ ATTR_QUEUE_SIZE_DESCRIPTION: Final = "Current size of question queue"
ATTR_API_STATUS_DESCRIPTION: Final = "Current API connection status" ATTR_API_STATUS_DESCRIPTION: Final = "Current API connection status"
ATTR_ERROR_COUNT_DESCRIPTION: Final = "Total number of errors" ATTR_ERROR_COUNT_DESCRIPTION: Final = "Total number of errors"
ATTR_LAST_ERROR_DESCRIPTION: Final = "Last error message" ATTR_LAST_ERROR_DESCRIPTION: Final = "Last error message"
ATTR_API_VERSION_DESCRIPTION: Final = "Current API version"
ATTR_ENDPOINT_STATUS_DESCRIPTION: Final = "Current endpoint status"
ATTR_REQUEST_COUNT_DESCRIPTION: Final = "Total number of API requests"
ATTR_TOKENS_USED_DESCRIPTION: Final = "Total tokens used"
# Entity attributes # Entity attributes
ENTITY_NAME: Final = "HA Text AI" ENTITY_NAME: Final = "HA Text AI"
ENTITY_ICON: Final = "mdi:robot" ENTITY_ICON: Final = "mdi:robot"
ENTITY_ICON_ERROR: Final = "mdi:robot-dead" ENTITY_ICON_ERROR: Final = "mdi:robot-dead"
ENTITY_ICON_PROCESSING: Final = "mdi:robot-excited" ENTITY_ICON_PROCESSING: Final = "mdi:robot-excited"
ENTITY_ICON_OFFLINE: Final = "mdi:robot-off"
ENTITY_ICON_QUEUE: Final = "mdi:robot-confused"
# Translation keys # Translation keys
TRANSLATION_KEY_CONFIG: Final = "config" TRANSLATION_KEY_CONFIG: Final = "config"
@@ -113,6 +157,10 @@ STATE_ERROR: Final = "error"
STATE_DISCONNECTED: Final = "disconnected" STATE_DISCONNECTED: Final = "disconnected"
STATE_RATE_LIMITED: Final = "rate_limited" STATE_RATE_LIMITED: Final = "rate_limited"
STATE_INITIALIZING: Final = "initializing" STATE_INITIALIZING: Final = "initializing"
STATE_MAINTENANCE: Final = "maintenance"
STATE_RETRYING: Final = "retrying"
STATE_QUEUED: Final = "queued"
STATE_UPDATING: Final = "updating"
# Logging # Logging
LOGGER_NAME: Final = "custom_components.ha_text_ai" LOGGER_NAME: Final = "custom_components.ha_text_ai"
@@ -122,11 +170,6 @@ LOG_LEVEL_DEFAULT: Final = "INFO"
QUEUE_TIMEOUT: Final = 5 QUEUE_TIMEOUT: Final = 5
QUEUE_MAX_SIZE: Final = 100 QUEUE_MAX_SIZE: Final = 100
# API constants
API_TIMEOUT: Final = 30
API_RETRY_COUNT: Final = 3
API_BACKOFF_FACTOR: Final = 1.5
# Service schema constants # Service schema constants
SCHEMA_QUESTION: Final = "question" SCHEMA_QUESTION: Final = "question"
SCHEMA_MODEL: Final = "model" SCHEMA_MODEL: Final = "model"
+476 -52
View File
@@ -2,14 +2,24 @@
import asyncio import asyncio
import logging import logging
from datetime import timedelta from datetime import timedelta
from typing import Any, Dict, Optional from typing import Any, Dict, Optional, List
import time
from openai import AsyncOpenAI, APIError, AuthenticationError, RateLimitError from openai import AsyncOpenAI, APIError, AuthenticationError, RateLimitError
from anthropic import AsyncAnthropic
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util
import async_timeout import async_timeout
from .const import DOMAIN from .const import (
DOMAIN,
DEFAULT_TIMEOUT,
MAX_QUEUE_SIZE,
MAX_RETRIES,
RETRY_DELAY,
SUPPORTED_MODELS,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -26,8 +36,21 @@ class HATextAICoordinator(DataUpdateCoordinator):
max_tokens: int, max_tokens: int,
request_interval: float, request_interval: float,
session: Optional[Any] = None, session: Optional[Any] = None,
is_anthropic: bool = False,
) -> None: ) -> None:
"""Initialize.""" """Initialize coordinator.
Args:
hass: HomeAssistant instance
api_key: API key for the service
endpoint: API endpoint URL
model: Model name to use
temperature: Temperature parameter for generation
max_tokens: Maximum tokens to generate
request_interval: Interval between requests
session: Optional session object
is_anthropic: Whether to use Anthropic API
"""
super().__init__( super().__init__(
hass, hass,
_LOGGER, _LOGGER,
@@ -42,18 +65,36 @@ class HATextAICoordinator(DataUpdateCoordinator):
self.model = model self.model = model
self.temperature = float(temperature) self.temperature = float(temperature)
self.max_tokens = int(max_tokens) self.max_tokens = int(max_tokens)
self._question_queue = asyncio.Queue() self._question_queue = asyncio.PriorityQueue(maxsize=MAX_QUEUE_SIZE)
self._responses: Dict[str, Any] = {} self._responses: Dict[str, Any] = {}
self.system_prompt: Optional[str] = None self.system_prompt: Optional[str] = None
self._is_ready = False self._is_ready = False
self._is_processing = False
self._is_rate_limited = False
self._is_maintenance = False
self._error_count = 0 self._error_count = 0
self._MAX_ERRORS = 3 self._MAX_ERRORS = 3
self._request_count = 0
self._tokens_used = 0
self._api_version = "v1"
self._endpoint_status = "disconnected"
self._performance_metrics: Dict[str, Any] = {
"avg_response_time": 0,
"total_errors": 0,
"success_rate": 100,
"requests_per_minute": 0,
}
self._last_request_time = 0
self._is_anthropic = is_anthropic
self.client = AsyncOpenAI( if is_anthropic:
api_key=self.api_key, self.client = AsyncAnthropic(api_key=self.api_key)
base_url=self.endpoint, else:
http_client=session, self.client = AsyncOpenAI(
) api_key=self.api_key,
base_url=self.endpoint,
http_client=session,
)
def _validate_params(self, api_key: str, temperature: float, max_tokens: int) -> None: def _validate_params(self, api_key: str, temperature: float, max_tokens: int) -> None:
"""Validate initialization parameters.""" """Validate initialization parameters."""
@@ -65,28 +106,59 @@ class HATextAICoordinator(DataUpdateCoordinator):
raise ValueError("Max tokens must be a positive integer") raise ValueError("Max tokens must be a positive integer")
async def _async_update_data(self) -> Dict[str, Any]: async def _async_update_data(self) -> Dict[str, Any]:
"""Update data via OpenAI API.""" """Update data via API."""
if self._question_queue.empty(): if self._question_queue.empty():
return self._responses return self._responses
try: try:
async with async_timeout.timeout(30): async with async_timeout.timeout(DEFAULT_TIMEOUT):
question = await self._question_queue.get() self._is_processing = True
priority, question_data = await self._question_queue.get()
question = question_data["question"]
params = question_data["params"]
try: try:
response_content = await self._make_api_call(question) response_data = await self._make_api_call(
question,
model=params.get("model"),
temperature=params.get("temperature"),
max_tokens=params.get("max_tokens"),
system_prompt=params.get("system_prompt")
)
self._update_metrics(response_data)
self._responses[question] = { self._responses[question] = {
"question": question, "question": question,
"response": response_content, "response": response_data["response"],
"error": None, "error": None,
"timestamp": self.hass.loop.time() "timestamp": dt_util.utcnow(),
"model": response_data["model"],
"temperature": params.get("temperature", self.temperature),
"max_tokens": params.get("max_tokens", self.max_tokens),
"response_time": response_data.get("response_time"),
"tokens": response_data.get("tokens", 0),
"priority": priority
} }
self._error_count = 0 self._error_count = 0
self._is_ready = True self._is_ready = True
self._endpoint_status = "connected"
self._request_count += 1
self._tokens_used += response_data.get("tokens", 0)
self._last_request_time = time.time()
# Fire event for successful response
self.hass.bus.async_fire(f"{DOMAIN}_response_received", {
"question": question,
"model": response_data["model"],
"tokens": response_data.get("tokens", 0)
})
_LOGGER.debug("Response received for question: %s", question) _LOGGER.debug("Response received for question: %s", question)
except Exception as err: except Exception as err:
self._handle_api_error(question, err) await self._handle_api_error(question, err)
finally: finally:
self._is_processing = False
self._question_queue.task_done() self._question_queue.task_done()
return self._responses return self._responses
@@ -96,26 +168,47 @@ class HATextAICoordinator(DataUpdateCoordinator):
await self._handle_timeout_error() await self._handle_timeout_error()
return self._responses return self._responses
def _handle_api_error(self, question: str, error: Exception) -> None: async def _handle_api_error(self, question: str, error: Exception) -> None:
"""Handle API errors.""" """Handle API errors with retry logic."""
self._error_count += 1 self._error_count += 1
self._performance_metrics["total_errors"] += 1
error_msg = str(error) error_msg = str(error)
if isinstance(error, AuthenticationError): if isinstance(error, AuthenticationError):
error_msg = "Authentication failed - invalid API key" error_msg = "Authentication failed - invalid API key"
self._is_ready = False self._is_ready = False
self._endpoint_status = "auth_error"
elif isinstance(error, RateLimitError): elif isinstance(error, RateLimitError):
error_msg = "Rate limit exceeded" error_msg = "Rate limit exceeded"
self._is_rate_limited = True
self._endpoint_status = "rate_limited"
# Implement exponential backoff
await asyncio.sleep(RETRY_DELAY * (2 ** (self._error_count - 1)))
elif isinstance(error, APIError): elif isinstance(error, APIError):
if "maintenance" in str(error).lower():
self._is_maintenance = True
self._endpoint_status = "maintenance"
error_msg = f"API error: {error}" error_msg = f"API error: {error}"
else:
self._endpoint_status = "error"
self._responses[question] = { self._responses[question] = {
"question": question, "question": question,
"response": None, "response": None,
"error": error_msg, "error": error_msg,
"timestamp": self.hass.loop.time() "timestamp": dt_util.utcnow(),
"model": self.model,
"temperature": self.temperature,
"max_tokens": self.max_tokens
} }
# Fire error event
self.hass.bus.async_fire(f"{DOMAIN}_error_occurred", {
"error_type": type(error).__name__,
"error_message": error_msg,
"question": question
})
_LOGGER.error("API error (%s): %s", type(error).__name__, error_msg) _LOGGER.error("API error (%s): %s", type(error).__name__, error_msg)
if self._error_count >= self._MAX_ERRORS: if self._error_count >= self._MAX_ERRORS:
@@ -124,71 +217,402 @@ class HATextAICoordinator(DataUpdateCoordinator):
self._error_count self._error_count
) )
async def _handle_timeout_error(self) -> None: def _update_metrics(self, response_data: Dict[str, Any]) -> None:
"""Handle timeout errors.""" """Update performance metrics."""
self._error_count += 1 response_time = response_data.get("response_time", 0)
if not self._question_queue.empty(): current_avg = self._performance_metrics["avg_response_time"]
try: self._performance_metrics["avg_response_time"] = (
# Clear the queue if we have timeout issues (current_avg * self._request_count + response_time) /
while not self._question_queue.empty(): (self._request_count + 1)
self._question_queue.get_nowait() )
self._question_queue.task_done()
except Exception as err:
_LOGGER.error("Error clearing question queue: %s", err)
async def _make_api_call(self, question: str) -> str: total_requests = self._request_count + 1
"""Make API call to OpenAI.""" self._performance_metrics["success_rate"] = (
(total_requests - self._performance_metrics["total_errors"]) /
total_requests * 100
)
# Calculate requests per minute
if self._last_request_time:
time_diff = time.time() - self._last_request_time
if time_diff > 0:
self._performance_metrics["requests_per_minute"] = 60 / time_diff
async def _make_api_call(
self,
question: str,
model: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
system_prompt: Optional[str] = None
) -> Dict[str, Any]:
"""Make API call to the selected service."""
try: try:
messages = [] start_time = dt_util.utcnow()
if self.system_prompt:
messages.append({"role": "system", "content": self.system_prompt})
messages.append({"role": "user", "content": question})
completion = await self.client.chat.completions.create( if self._is_anthropic:
model=self.model, response = await self._make_anthropic_call(
messages=messages, question, model, temperature, max_tokens, system_prompt
temperature=self.temperature, )
max_tokens=self.max_tokens, else:
) response = await self._make_openai_call(
return completion.choices[0].message.content question, model, temperature, max_tokens, system_prompt
)
response_time = (dt_util.utcnow() - start_time).total_seconds()
return {
**response,
"response_time": response_time
}
except Exception as err: except Exception as err:
_LOGGER.error("Error in API call: %s", err) _LOGGER.error("Error in API call: %s", err)
raise raise
async def async_ask_question(self, question: str) -> None: async def _make_anthropic_call(
"""Add question to queue.""" self,
question: str,
model: Optional[str],
temperature: Optional[float],
max_tokens: Optional[int],
system_prompt: Optional[str]
) -> Dict[str, Any]:
"""Make API call to Anthropic."""
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": question})
completion = await self.client.messages.create(
model=model or self.model,
messages=messages,
temperature=temperature if temperature is not None else self.temperature,
max_tokens=max_tokens if max_tokens is not None else self.max_tokens,
)
return {
"response": completion.content[0].text,
"model": completion.model,
"tokens": completion.usage.total_tokens if hasattr(completion, 'usage') else 0
}
async def _make_openai_call(
self,
question: str,
model: Optional[str],
temperature: Optional[float],
max_tokens: Optional[int],
system_prompt: Optional[str]
) -> Dict[str, Any]:
"""Make API call to OpenAI."""
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": question})
completion = await self.client.chat.completions.create(
model=model or self.model,
messages=messages,
temperature=temperature if temperature is not None else self.temperature,
max_tokens=max_tokens if max_tokens is not None else self.max_tokens,
)
return {
"response": completion.choices[0].message.content,
"model": completion.model,
"tokens": completion.usage.total_tokens if hasattr(completion, 'usage') else 0
}
async def async_ask_question(
self,
question: str,
system_prompt: Optional[str] = None,
model: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
priority: bool = False
) -> None:
"""Add question to queue with priority support."""
if not self._is_ready and self._error_count >= self._MAX_ERRORS: if not self._is_ready and self._error_count >= self._MAX_ERRORS:
_LOGGER.warning("Coordinator is not ready due to previous errors") _LOGGER.warning("Coordinator is not ready due to previous errors")
return return
await self._question_queue.put(question) question_data = {
await self.async_refresh() "question": question,
"params": {
"system_prompt": system_prompt,
"model": model,
"temperature": temperature,
"max_tokens": max_tokens
}
}
# Priority: 0 for high priority, 1 for normal
priority_level = 0 if priority else 1
try:
await self._question_queue.put((priority_level, question_data))
await self.async_refresh()
except asyncio.QueueFull:
_LOGGER.error("Question queue is full. Try again later.")
raise RuntimeError("Queue is full")
async def async_shutdown(self) -> None: async def async_shutdown(self) -> None:
"""Shutdown the coordinator.""" """Shutdown the coordinator."""
try: try:
# Clear the queue
while not self._question_queue.empty(): while not self._question_queue.empty():
self._question_queue.get_nowait() try:
self._question_queue.task_done() self._question_queue.get_nowait()
self._question_queue.task_done()
except asyncio.QueueEmpty:
break
if hasattr(self.client, 'close'):
await self.client.close()
await self.client.close()
self._is_ready = False self._is_ready = False
self._endpoint_status = "disconnected"
# Final metrics update
self._update_final_metrics()
except Exception as err: except Exception as err:
_LOGGER.error("Error during shutdown: %s", err) _LOGGER.error("Error during shutdown: %s", err)
def _update_final_metrics(self) -> None:
"""Update final metrics before shutdown."""
if self._request_count > 0:
self._performance_metrics["final_success_rate"] = (
(self._request_count - self._performance_metrics["total_errors"]) /
self._request_count * 100
)
self._performance_metrics["total_requests"] = self._request_count
self._performance_metrics["total_tokens"] = self._tokens_used
@property
def performance_metrics(self) -> Dict[str, Any]:
"""Return current performance metrics."""
return self._performance_metrics
@property
def queue_size(self) -> int:
"""Return current queue size."""
return self._question_queue.qsize()
@property
def is_queue_full(self) -> bool:
"""Return whether queue is full."""
return self._question_queue.full()
@property @property
def is_ready(self) -> bool: def is_ready(self) -> bool:
"""Return if coordinator is ready.""" """Return if coordinator is ready."""
return self._is_ready return self._is_ready and self._error_count < self._MAX_ERRORS
@property
def is_processing(self) -> bool:
"""Return if coordinator is processing."""
return self._is_processing
@property
def is_rate_limited(self) -> bool:
"""Return if coordinator is rate limited."""
return self._is_rate_limited
@property
def is_maintenance(self) -> bool:
"""Return if API is in maintenance."""
return self._is_maintenance
@property @property
def error_count(self) -> int: def error_count(self) -> int:
"""Return current error count.""" """Return current error count."""
return self._error_count return self._error_count
@property
def request_count(self) -> int:
"""Return total request count."""
return self._request_count
@property
def tokens_used(self) -> int:
"""Return total tokens used."""
return self._tokens_used
@property
def api_version(self) -> str:
"""Return API version."""
return self._api_version
@property
def endpoint_status(self) -> str:
"""Return endpoint status."""
return self._endpoint_status
@property
def responses(self) -> Dict[str, Any]:
"""Return all responses."""
return self._responses
@property
def last_response(self) -> Optional[Dict[str, Any]]:
"""Return the last response."""
if not self._responses:
return None
return next(iter(self._responses.values()))
def reset_error_count(self) -> None: def reset_error_count(self) -> None:
"""Reset error counter.""" """Reset error counter."""
self._error_count = 0 self._error_count = 0
self._is_rate_limited = False
self._is_maintenance = False
if not self._is_ready:
self._is_ready = True
self._endpoint_status = "connected"
async def clear_queue(self) -> None:
"""Clear the question queue."""
try:
while not self._question_queue.empty():
try:
self._question_queue.get_nowait()
self._question_queue.task_done()
except asyncio.QueueEmpty:
break
except Exception as err:
_LOGGER.error("Error clearing queue: %s", err)
async def clear_history(self) -> None:
"""Clear response history."""
self._responses.clear()
await self.async_refresh()
def get_response(self, question: str) -> Optional[Dict[str, Any]]:
"""Get specific response by question."""
return self._responses.get(question)
def get_recent_responses(self, limit: int = 10) -> List[Dict[str, Any]]:
"""Get most recent responses."""
return list(sorted(
self._responses.values(),
key=lambda x: x["timestamp"],
reverse=True
))[:limit]
async def retry_failed_requests(self) -> None:
"""Retry failed requests."""
failed_requests = [
(q, r) for q, r in self._responses.items()
if r.get("error") is not None
]
for question, response in failed_requests:
await self.async_ask_question(
question,
system_prompt=response.get("system_prompt"),
model=response.get("model"),
temperature=response.get("temperature"),
max_tokens=response.get("max_tokens"),
priority=True
)
def update_system_prompt(self, new_prompt: str) -> None:
"""Update system prompt."""
self.system_prompt = new_prompt
_LOGGER.info("System prompt updated")
async def health_check(self) -> Dict[str, Any]:
"""Perform health check."""
health_status = {
"is_ready": self.is_ready,
"is_processing": self.is_processing,
"is_rate_limited": self.is_rate_limited,
"is_maintenance": self.is_maintenance,
"error_count": self.error_count,
"endpoint_status": self.endpoint_status,
"queue_size": self.queue_size,
"request_count": self.request_count,
"tokens_used": self.tokens_used,
"performance_metrics": self.performance_metrics
}
return health_status
async def _handle_timeout_error(self) -> None:
"""Handle timeout errors."""
self._error_count += 1
self._endpoint_status = "timeout"
self._performance_metrics["total_errors"] += 1
if not self._question_queue.empty():
await self.clear_queue()
# Fire timeout event
self.hass.bus.async_fire(f"{DOMAIN}_timeout_error", {
"error_count": self._error_count,
"endpoint_status": self._endpoint_status
})
def export_metrics(self) -> Dict[str, Any]:
"""Export all metrics and statistics."""
return {
"performance": self._performance_metrics,
"requests": {
"total": self._request_count,
"successful": self._request_count - self._performance_metrics["total_errors"],
"failed": self._performance_metrics["total_errors"]
},
"tokens": {
"total_used": self._tokens_used,
"average_per_request": self._tokens_used / self._request_count if self._request_count > 0 else 0
},
"status": {
"is_ready": self.is_ready,
"endpoint_status": self._endpoint_status,
"error_count": self._error_count
},
"queue": {
"size": self.queue_size,
"is_full": self.is_queue_full
}
}
async def validate_model(self, model: str) -> bool:
"""Validate if model is supported."""
return model in SUPPORTED_MODELS
async def estimate_tokens(self, text: str) -> int:
"""Estimate token count for text."""
# Простая оценка: примерно 4 символа на токен
return len(text) // 4
def get_rate_limit_info(self) -> Dict[str, Any]:
"""Get rate limit information."""
return {
"is_rate_limited": self._is_rate_limited,
"retry_after": RETRY_DELAY * (2 ** (self._error_count - 1)) if self._is_rate_limited else 0
}
async def optimize_queue(self) -> None:
"""Optimize queue by removing duplicate requests."""
if self._question_queue.empty():
return
seen_questions = set()
optimized_queue = asyncio.PriorityQueue(maxsize=MAX_QUEUE_SIZE)
while not self._question_queue.empty():
try:
priority, question_data = self._question_queue.get_nowait()
question = question_data["question"]
if question not in seen_questions:
seen_questions.add(question)
await optimized_queue.put((priority, question_data))
self._question_queue.task_done()
except asyncio.QueueEmpty:
break
self._question_queue = optimized_queue
+32 -4
View File
@@ -4,11 +4,39 @@
"codeowners": ["@smkrv"], "codeowners": ["@smkrv"],
"config_flow": true, "config_flow": true,
"dependencies": [], "dependencies": [],
"documentation": "https://github.com/smkrv/ha-text-ai/wiki", "documentation": "https://github.com/smkrv/ha-text-ai",
"homepage": "https://github.com/smkrv/ha-text-ai",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"issue_tracker": "https://github.com/smkrv/ha-text-ai/issues", "issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
"requirements": ["openai>=1.0.0"], "requirements": [
"openai>=1.12.0",
"anthropic>=0.8.0",
"aiohttp>=3.8.0",
"async-timeout>=4.0.0"
],
"version": "2.0.0",
"homeassistant": "2024.1.0",
"quality_scale": "silver",
"integration_type": "service",
"translations": [
"en"
],
"zeroconf": [],
"ssdp": [], "ssdp": [],
"version": "1.0.5", "usb": [],
"zeroconf": [] "bluetooth": [],
"mqtt": [],
"loggers": ["custom_components.ha_text_ai"],
"after_dependencies": ["http"],
"title": "Text AI Assistant",
"description": "AI-powered text assistant supporting multiple models including GPT and Claude",
"render_readme": true,
"tags": [
"ai",
"automation",
"chat",
"assistant",
"openai",
"claude"
]
} }
+64 -10
View File
@@ -30,6 +30,10 @@ from .const import (
ATTR_ERROR_COUNT, ATTR_ERROR_COUNT,
ATTR_LAST_ERROR, ATTR_LAST_ERROR,
ATTR_RESPONSE_TIME, ATTR_RESPONSE_TIME,
ATTR_API_VERSION,
ATTR_ENDPOINT_STATUS,
ATTR_REQUEST_COUNT,
ATTR_TOKENS_USED,
ENTITY_ICON, ENTITY_ICON,
ENTITY_ICON_ERROR, ENTITY_ICON_ERROR,
ENTITY_ICON_PROCESSING, ENTITY_ICON_PROCESSING,
@@ -39,6 +43,7 @@ from .const import (
STATE_DISCONNECTED, STATE_DISCONNECTED,
STATE_RATE_LIMITED, STATE_RATE_LIMITED,
STATE_INITIALIZING, STATE_INITIALIZING,
STATE_MAINTENANCE,
) )
from .coordinator import HATextAICoordinator from .coordinator import HATextAICoordinator
@@ -53,6 +58,7 @@ async def async_setup_entry(
coordinator = hass.data[DOMAIN][entry.entry_id] coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([HATextAISensor(coordinator, entry)], True) async_add_entities([HATextAISensor(coordinator, entry)], True)
class HATextAISensor(CoordinatorEntity, SensorEntity): class HATextAISensor(CoordinatorEntity, SensorEntity):
"""HA text AI Sensor.""" """HA text AI Sensor."""
@@ -74,6 +80,13 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
self._error_count = 0 self._error_count = 0
self._last_error = None self._last_error = None
self._state = STATE_INITIALIZING self._state = STATE_INITIALIZING
self._attr_device_info = {
"identifiers": {(DOMAIN, self._attr_unique_id)},
"name": "HA Text AI",
"manufacturer": "Community",
"model": coordinator.model,
"sw_version": coordinator.api_version,
}
@property @property
def icon(self) -> str: def icon(self) -> str:
@@ -87,13 +100,16 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
@property @property
def state(self) -> StateType: def state(self) -> StateType:
"""Return the state of the sensor.""" """Return the state of the sensor."""
if not self.coordinator.data or not self.coordinator.last_update_success_time: if not self.coordinator.data or not self.coordinator.last_update_success:
return None return None
try: try:
if isinstance(self.coordinator.last_update_success_time, datetime): if self.coordinator.data and isinstance(self.coordinator.data, dict):
return dt_util.as_local(self.coordinator.last_update_success_time) last_update = self.coordinator.data.get("last_update")
return self.coordinator.last_update_success_time if isinstance(last_update, datetime):
return dt_util.as_local(last_update)
return last_update
return None
except Exception as err: except Exception as err:
_LOGGER.error("Error getting state: %s", err, exc_info=True) _LOGGER.error("Error getting state: %s", err, exc_info=True)
return None return None
@@ -111,27 +127,41 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
ATTR_API_STATUS: self._state, ATTR_API_STATUS: self._state,
ATTR_ERROR_COUNT: self._error_count, ATTR_ERROR_COUNT: self._error_count,
ATTR_LAST_ERROR: self._last_error, ATTR_LAST_ERROR: self._last_error,
ATTR_API_VERSION: self.coordinator.api_version,
ATTR_ENDPOINT_STATUS: self.coordinator.endpoint_status,
ATTR_REQUEST_COUNT: self.coordinator.request_count,
ATTR_TOKENS_USED: self.coordinator.tokens_used,
} }
if not self.coordinator.data: if not self.coordinator.data:
return attributes return attributes
try: try:
history = list(self.coordinator.data.items()) history = list(self.coordinator._responses.items())
if history: if history:
last_question, last_data = history[-1] last_question, last_data = history[-1]
# Handle different response formats
if isinstance(last_data, dict): if isinstance(last_data, dict):
last_response = last_data.get("response", "") last_response = last_data.get("response", "")
last_updated = last_data.get("timestamp", self.coordinator.last_update_success_time) last_updated = last_data.get("timestamp") or self.coordinator.data.get("last_update")
response_time = last_data.get("response_time") response_time = last_data.get("response_time")
model = last_data.get("model", self.coordinator.model)
temperature = last_data.get("temperature", self.coordinator.temperature)
max_tokens = last_data.get("max_tokens", self.coordinator.max_tokens)
error = last_data.get("error")
if error:
self._last_error = error
self._state = STATE_ERROR
else: else:
last_response = str(last_data) last_response = str(last_data)
last_updated = self.coordinator.last_update_success_time last_updated = self.coordinator.data.get("last_update")
response_time = None response_time = None
model = self.coordinator.model
temperature = self.coordinator.temperature
max_tokens = self.coordinator.max_tokens
# Convert timestamp to local time if needed
if isinstance(last_updated, datetime): if isinstance(last_updated, datetime):
last_updated = dt_util.as_local(last_updated) last_updated = dt_util.as_local(last_updated)
@@ -140,6 +170,9 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
ATTR_RESPONSE: last_response, ATTR_RESPONSE: last_response,
ATTR_LAST_UPDATED: last_updated, ATTR_LAST_UPDATED: last_updated,
ATTR_TOTAL_RESPONSES: len(history), ATTR_TOTAL_RESPONSES: len(history),
ATTR_MODEL: model,
ATTR_TEMPERATURE: temperature,
ATTR_MAX_TOKENS: max_tokens,
}) })
if response_time is not None: if response_time is not None:
@@ -169,9 +202,24 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
try: try:
if self.coordinator.data: if self.coordinator.data:
self._state = STATE_READY if self.coordinator._is_ready:
if self.coordinator._is_processing:
self._state = STATE_PROCESSING
elif self.coordinator._is_rate_limited:
self._state = STATE_RATE_LIMITED
elif self.coordinator._is_maintenance:
self._state = STATE_MAINTENANCE
else:
self._state = STATE_READY
else:
self._state = STATE_DISCONNECTED
else: else:
self._state = STATE_DISCONNECTED self._state = STATE_DISCONNECTED
# Обновляем счетчик ошибок только если статус изменился на ошибку
if self._state == STATE_ERROR:
self._error_count += 1
except Exception as err: except Exception as err:
_LOGGER.error("Error handling update: %s", err, exc_info=True) _LOGGER.error("Error handling update: %s", err, exc_info=True)
self._error_count += 1 self._error_count += 1
@@ -179,3 +227,9 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
self._state = STATE_ERROR self._state = STATE_ERROR
self.async_write_ha_state() self.async_write_ha_state()
async def async_reset_error_count(self) -> None:
"""Reset the error counter."""
self._error_count = 0
self._last_error = None
self.async_write_ha_state()
+62 -27
View File
@@ -4,6 +4,7 @@ ask_question:
Send a question to the AI model and receive a detailed response. 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. The response will be stored in the conversation history and can be retrieved later.
Response time may vary based on model selection and server load. Response time may vary based on model selection and server load.
Supports various AI models with different capabilities and pricing.
fields: fields:
question: question:
name: Question name: Question
@@ -11,6 +12,7 @@ ask_question:
Your question or prompt for the AI assistant. Be specific and clear for better results. 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. You can ask about home automation, technical advice, or general questions.
For complex queries, consider breaking them into smaller parts. For complex queries, consider breaking them into smaller parts.
The system will maintain conversation context for follow-up questions.
required: true required: true
example: | example: |
What automations would you recommend for a smart kitchen? What automations would you recommend for a smart kitchen?
@@ -28,13 +30,17 @@ ask_question:
name: Model name: Model
description: >- description: >-
Select an AI model to use (optional, overrides default setting). Select an AI model to use (optional, overrides default setting).
Different models have different capabilities and token limits. Different models have different capabilities, context limits, and response characteristics:
- GPT-3.5 Turbo: Fast, efficient, good for most tasks
- GPT-4: More capable, better reasoning, slower
- Claude-3: Advanced capabilities, longer context
Note: More capable models may have longer response times and higher API costs. Note: More capable models may have longer response times and higher API costs.
required: false required: false
example: "gpt-3.5-turbo" example: "gpt-3.5-turbo"
default: "gpt-3.5-turbo" default: "gpt-3.5-turbo"
selector: selector:
select: select:
custom_value: true
options: options:
- label: "GPT-3.5 Turbo (Fast & Efficient)" - label: "GPT-3.5 Turbo (Fast & Efficient)"
value: "gpt-3.5-turbo" value: "gpt-3.5-turbo"
@@ -46,12 +52,16 @@ ask_question:
value: "gpt-4-32k" value: "gpt-4-32k"
- label: "GPT-4 Turbo (Latest)" - label: "GPT-4 Turbo (Latest)"
value: "gpt-4-1106-preview" value: "gpt-4-1106-preview"
- label: "Claude-3 Sonnet (Balanced)"
value: "claude-3-sonnet"
- label: "Claude-3 Opus (Most Advanced)"
value: "claude-3-opus"
mode: dropdown mode: dropdown
temperature: temperature:
name: Temperature name: Temperature
description: >- description: >-
Controls response creativity (0-2): Controls response creativity (0.0-2.0):
0.0-0.3: Focused, consistent responses (best for technical/factual queries) 0.0-0.3: Focused, consistent responses (best for technical/factual queries)
0.4-0.7: Balanced responses (recommended for most uses) 0.4-0.7: Balanced responses (recommended for most uses)
0.8-2.0: More creative, varied responses (best for brainstorming) 0.8-2.0: More creative, varied responses (best for brainstorming)
@@ -69,12 +79,12 @@ ask_question:
max_tokens: max_tokens:
name: Max Tokens name: Max Tokens
description: >- description: >-
Maximum length of the response. Higher values allow longer responses but use more API tokens. Maximum length of the response (1-4096 tokens).
Recommended ranges: Recommended ranges:
- Short responses (256-512): Quick answers, status updates - Short (256-512): Quick answers, status updates
- Medium responses (512-1024): Detailed explanations, instructions - Medium (512-1024): Detailed explanations
- Long responses (1024-4096): Complex analysis, multiple examples - Long (1024-4096): Complex analysis
Note: Actual response length may be shorter based on content. Note: Token limits vary by model. Actual length may be shorter.
required: false required: false
default: 1000 default: 1000
selector: selector:
@@ -84,19 +94,35 @@ ask_question:
step: 1 step: 1
mode: box mode: box
system_prompt:
name: System Prompt
description: >-
Optional system prompt to set context for this specific question.
This will temporarily override the default system prompt.
Use this to specify expertise areas, response format, or special instructions.
required: false
example: "You are a home automation expert focused on energy efficiency and security"
selector:
text:
multiline: true
clear_history: clear_history:
name: Clear History name: Clear History
description: >- description: >-
Delete all stored questions and responses from the conversation history. Delete all stored questions and responses from the conversation history.
This action cannot be undone. Consider using 'get_history' first if you need to backup the data. This action cannot be undone. Consider using 'get_history' first if you need to backup the data.
System prompt settings will be preserved. System prompt settings and configuration will be preserved.
fields: {} fields: {}
get_history: get_history:
name: Get History name: Get History
description: >- description: >-
Retrieve recent conversation history, including questions, responses, and timestamps. Retrieve recent conversation history, including:
Results are ordered from newest to oldest and include metadata like model used and response times. - Questions and responses
- Timestamps and response times
- Models used and token counts
- Temperature and other settings
Results are ordered from newest to oldest.
fields: fields:
limit: limit:
name: Limit name: Limit
@@ -121,6 +147,7 @@ get_history:
required: false required: false
selector: selector:
select: select:
custom_value: true
options: options:
- label: "All Models" - label: "All Models"
value: "" value: ""
@@ -128,21 +155,41 @@ get_history:
value: "gpt-3.5-turbo" value: "gpt-3.5-turbo"
- label: "GPT-4" - label: "GPT-4"
value: "gpt-4" value: "gpt-4"
- label: "GPT-4 Turbo"
value: "gpt-4-1106-preview"
- label: "Claude-3 Sonnet"
value: "claude-3-sonnet"
- label: "Claude-3 Opus"
value: "claude-3-opus"
mode: dropdown mode: dropdown
start_date:
name: Start Date
description: >-
Optional start date for filtering history.
Format: YYYY-MM-DD
required: false
selector:
datetime:
type: date
set_system_prompt: set_system_prompt:
name: Set System Prompt name: Set System Prompt
description: >- description: >-
Configure the AI's behavior by setting a system prompt. Set default system behavior instructions for all future conversations.
This affects how the AI interprets and responds to all future questions. This defines how the AI should behave and respond to questions.
The prompt will persist until changed or cleared. The prompt will be used for all models unless overridden per question.
fields: fields:
prompt: prompt:
name: System Prompt name: System Prompt
description: >- description: >-
Instructions that define how the AI should behave and respond. Instructions that define how the AI should behave and respond.
Be specific about the desired expertise, tone, and format of responses. Be specific about:
Maximum length: 1000 characters. - Desired expertise and knowledge areas
- Response tone and style
- Output format preferences
- Special handling instructions
Maximum length: 1000 characters
required: true required: true
example: | example: |
You are a home automation expert assistant. Focus on: You are a home automation expert assistant. Focus on:
@@ -156,15 +203,3 @@ set_system_prompt:
selector: selector:
text: text:
multiline: true multiline: true
type: text
max_length: 1000
clear_prompt:
name: Clear Existing Prompt
description: >-
Set to true to remove the current system prompt before applying the new one.
This ensures no conflicting instructions remain.
required: false
default: false
selector:
boolean: {}
+141 -186
View File
@@ -1,192 +1,147 @@
{ {
"config": { "config": {
"step": { "step": {
"user": { "user": {
"title": "Set up HA Text AI", "title": "Set up HA Text AI",
"description": "Configure your OpenAI integration for smart home interactions. You'll need an OpenAI API key from platform.openai.com to proceed.", "description": "Configure your AI integration for various providers (OpenAI, Anthropic, etc.). Different models have different capabilities and pricing.",
"data": { "data": {
"api_key": { "api_key": "API key for authentication (required)",
"name": "OpenAI API Key", "model": "AI model to use (e.g., gpt-3.5-turbo, gpt-4, claude-3-sonnet, claude-3-opus)",
"description": "Your OpenAI API key from platform.openai.com. Keep this secure and never share it." "temperature": "Response creativity (0-2, lower = more focused and consistent)",
}, "max_tokens": "Maximum response length (1-4096 tokens)",
"model": { "api_endpoint": "Custom API endpoint URL (default varies by provider)",
"name": "AI Model", "request_interval": "Minimum time between requests in seconds (min: 0.1)",
"description": "Select the AI model to use. GPT-3.5-Turbo is recommended for most uses as it offers the best balance of capabilities and cost." "system_prompt": "Default instructions for AI behavior and expertise"
}, }
"temperature": { }
"name": "Temperature", },
"description": "Controls response creativity (0-2). Low values (0.1-0.3) for focused responses, high values (0.8-2.0) for creative ones." "error": {
}, "invalid_auth": "Authentication failed - check your API key",
"max_tokens": { "invalid_api_key": "Invalid API key - please verify your credentials",
"name": "Max Tokens", "cannot_connect": "Connection failed - check endpoint and network status",
"description": "Maximum length of responses. Higher values allow longer responses but consume more API tokens. Recommended: 512-1024." "invalid_model": "Model unavailable or not supported by the API",
}, "rate_limit": "Rate limit exceeded - please reduce request frequency",
"api_endpoint": { "context_length": "Input exceeds maximum context length for model",
"name": "API Endpoint", "api_error": "API service error - check provider status",
"description": "OpenAI API endpoint URL. Leave default unless using a custom endpoint or proxy." "timeout": "Request timeout - server not responding",
}, "queue_full": "Request queue full - try again later",
"request_interval": { "invalid_prompt": "Invalid system prompt format or length",
"name": "Request Interval", "invalid_url_format": "Invalid API endpoint URL format",
"description": "Minimum time between API requests in seconds. Increase if experiencing rate limits." "invalid_input": "Invalid configuration parameters",
} "unknown": "Unexpected error - check logs for details"
} }
}
}, },
"error": { "options": {
"invalid_auth": "Invalid API key. Please check your OpenAI API key and try again.", "step": {
"cannot_connect": "Failed to connect to API. Please check your internet connection and API endpoint.", "init": {
"unknown": "Unexpected error occurred. Please check the logs for more details.", "title": "HA Text AI Settings",
"already_exists": "This API key is already configured in another integration.", "description": "Adjust your AI integration parameters and behavior",
"invalid_model": "Selected model is not available. Please choose a different model.", "data": {
"rate_limit": "API rate limit exceeded. Please try again later or increase the request interval.", "model": "Select AI model for responses (capabilities vary)",
"context_length": "Input too long for selected model. Try reducing max tokens or using a model with larger context.", "temperature": "Response creativity (0-2, affects variation)",
"api_error": "OpenAI API error. Please check the logs for details.", "max_tokens": "Maximum response length in tokens",
"timeout": "API response timeout. Request took too long to complete.", "request_interval": "Minimum seconds between requests (rate limiting)",
"queue_full": "Request queue is full. Please try again later." "system_prompt": "Default AI behavior and expertise instructions",
"timeout": "Request timeout in seconds (default: 30)",
"retry_count": "Number of retry attempts for failed requests",
"queue_size": "Maximum pending requests in queue"
}
}
}
}, },
"abort": { "services": {
"already_configured": "This OpenAI integration is already configured", "ask_question": {
"auth_failed": "Authentication failed. Please verify your API key.", "name": "Ask Question",
"invalid_endpoint": "Invalid API endpoint URL provided" "description": "Send a question to the AI model and get a detailed response. Supports context awareness for follow-up questions.",
"fields": {
"question": {
"name": "Question",
"description": "Your question or prompt for the AI model. Be specific for better results."
},
"system_prompt": {
"name": "System Prompt",
"description": "Optional behavior instructions for this specific question"
},
"model": {
"name": "Model",
"description": "Optional specific AI model for this request (overrides default)"
},
"temperature": {
"name": "Temperature",
"description": "Optional creativity setting (0-2, affects response variation)"
},
"max_tokens": {
"name": "Max Tokens",
"description": "Optional maximum response length (1-4096 tokens)"
}
}
},
"clear_history": {
"name": "Clear History",
"description": "Delete all stored conversation history, responses, and metadata"
},
"get_history": {
"name": "Get History",
"description": "Retrieve conversation history with detailed metadata including timestamps",
"fields": {
"limit": {
"name": "Limit",
"description": "Maximum number of conversations to return (1-100)"
},
"filter_model": {
"name": "Filter Model",
"description": "Show only responses from a specific model"
},
"start_date": {
"name": "Start Date",
"description": "Filter conversations from this date (YYYY-MM-DD)"
}
}
},
"set_system_prompt": {
"name": "Set System Prompt",
"description": "Update default AI behavior and expertise instructions",
"fields": {
"prompt": {
"name": "System Prompt",
"description": "Instructions for AI behavior, expertise, and response style"
}
}
}
},
"state": {
"ready": "Ready for requests",
"processing": "Processing request",
"error": "Error occurred",
"disconnected": "API disconnected",
"rate_limited": "Rate limit reached",
"initializing": "Starting up",
"retrying": "Retrying request",
"queued": "Request queued"
},
"entity": {
"sensor": {
"status": {
"name": "AI Status",
"state": {
"ready": "Ready",
"processing": "Processing",
"error": "Error",
"disconnected": "Disconnected",
"rate_limited": "Rate Limited",
"initializing": "Initializing",
"retrying": "Retrying",
"queued": "Queued"
}
},
"last_response": {
"name": "Last Response",
"state": {
"success": "Success",
"error": "Error",
"timeout": "Timeout"
}
}
}
} }
},
"options": {
"step": {
"init": {
"title": "HA Text AI Options",
"description": "Adjust your OpenAI integration settings. Changes will apply to future requests only.",
"data": {
"temperature": {
"name": "Temperature",
"description": "Controls response creativity (0-2). Low values for focused responses, high for creative ones."
},
"max_tokens": {
"name": "Max Tokens",
"description": "Maximum length of responses. Higher values allow longer responses but consume more API tokens."
},
"request_interval": {
"name": "Request Interval",
"description": "Minimum time between API requests in seconds. Increase if experiencing rate limits."
}
}
}
}
},
"entity": {
"sensor": {
"last_response": {
"name": "Last Response",
"state_attributes": {
"last_updated": {
"name": "Last Updated",
"description": "Timestamp of the last AI response"
},
"question": {
"name": "Last Question",
"description": "Most recent question asked"
},
"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"
},
"queue_size": {
"name": "Queue Size",
"description": "Current size of request queue"
},
"api_status": {
"name": "API Status",
"description": "Current API connection status"
},
"error_count": {
"name": "Error Count",
"description": "Number of errors since last reset"
},
"last_error": {
"name": "Last Error",
"description": "Description of the last error encountered"
}
}
}
}
},
"services": {
"ask_question": {
"name": "Ask Question",
"description": "Send a question to the AI model and receive a detailed response. The response will be stored in conversation history.",
"fields": {
"question": {
"name": "Question",
"description": "Your question or prompt for the AI. Be specific for better results."
},
"model": {
"name": "Model",
"description": "AI model to use (optional, overrides default settings)."
},
"temperature": {
"name": "Temperature",
"description": "Response creativity level (0-2, optional)."
},
"max_tokens": {
"name": "Max Tokens",
"description": "Maximum response length (optional)."
}
}
},
"clear_history": {
"name": "Clear History",
"description": "Delete all stored conversation history. This action cannot be undone."
},
"get_history": {
"name": "Get History",
"description": "Retrieve conversation history, including questions, responses, and timestamps.",
"fields": {
"limit": {
"name": "Limit",
"description": "Number of recent conversations to return (default 10)."
},
"filter_model": {
"name": "Filter by Model",
"description": "Retrieve only conversations using a specific AI model."
}
}
},
"set_system_prompt": {
"name": "Set System Prompt",
"description": "Configure AI behavior by setting a system prompt.",
"fields": {
"prompt": {
"name": "Prompt",
"description": "Instructions defining AI behavior and response style."
},
"clear_prompt": {
"name": "Clear Prompt",
"description": "Remove current system prompt before setting new one."
}
}
}
}
} }
@@ -1,192 +0,0 @@
{
"config": {
"step": {
"user": {
"title": "Настройка HA Text AI",
"description": "Настройте интеграцию OpenAI для умного дома. Требуется API ключ OpenAI. Подробнее о получении ключа на platform.openai.com",
"data": {
"api_key": {
"name": "API ключ OpenAI",
"description": "Ваш API ключ с platform.openai.com. Храните его в безопасности."
},
"model": {
"name": "AI Модель",
"description": "Выберите модель AI. GPT-3.5-Turbo рекомендуется для большинства задач как оптимальное сочетание возможностей и стоимости."
},
"temperature": {
"name": "Температура",
"description": "Контролирует креативность ответов (0-2). Низкие значения (0.1-0.3) для точных ответов, высокие (0.8-2.0) для творческих."
},
"max_tokens": {
"name": "Максимум токенов",
"description": "Максимальная длина ответов. Больше токенов = длиннее ответы, но выше расход API токенов. Рекомендуется: 512-1024."
},
"api_endpoint": {
"name": "API Endpoint",
"description": "URL API OpenAI. Оставьте значение по умолчанию, если не используете собственный endpoint."
},
"request_interval": {
"name": "Интервал запросов",
"description": "Минимальное время между API запросами в секундах. Увеличьте при превышении лимитов запросов."
}
}
}
},
"error": {
"invalid_auth": "Неверный API ключ. Проверьте ключ OpenAI и попробуйте снова.",
"cannot_connect": "Не удалось подключиться к API. Проверьте подключение к интернету и endpoint.",
"unknown": "Неожиданная ошибка. Проверьте логи для подробностей.",
"already_exists": "Этот API ключ уже используется в другой интеграции.",
"invalid_model": "Выбранная модель недоступна. Выберите другую модель.",
"rate_limit": "Превышен лимит API запросов. Попробуйте позже или увеличьте интервал запросов.",
"context_length": "Входные данные слишком длинные для выбранной модели. Уменьшите max_tokens или используйте модель с большим контекстом.",
"api_error": "Ошибка API OpenAI. Проверьте логи для подробностей.",
"timeout": "Превышено время ожидания ответа от API.",
"queue_full": "Очередь запросов переполнена. Попробуйте позже."
},
"abort": {
"already_configured": "Эта интеграция OpenAI уже настроена",
"auth_failed": "Ошибка аутентификации. Проверьте API ключ.",
"invalid_endpoint": "Указан неверный URL API endpoint"
}
},
"options": {
"step": {
"init": {
"title": "Настройки HA Text AI",
"description": "Измените настройки интеграции OpenAI. Изменения применятся к будущим запросам.",
"data": {
"temperature": {
"name": "Температура",
"description": "Контролирует креативность ответов (0-2). Низкие значения для точных ответов, высокие для творческих."
},
"max_tokens": {
"name": "Максимум токенов",
"description": "Максимальная длина ответов. Больше токенов = длиннее ответы, но выше расход API токенов."
},
"request_interval": {
"name": "Интервал запросов",
"description": "Минимальное время между API запросами в секундах. Увеличьте при превышении лимитов."
}
}
}
}
},
"entity": {
"sensor": {
"last_response": {
"name": "Последний ответ",
"state_attributes": {
"last_updated": {
"name": "Последнее обновление",
"description": "Время последнего ответа AI"
},
"question": {
"name": "Последний вопрос",
"description": "Последний заданный вопрос"
},
"response": {
"name": "Ответ AI",
"description": "Последний ответ от AI"
},
"model": {
"name": "Текущая модель",
"description": "Используемая модель AI"
},
"temperature": {
"name": "Настройка температуры",
"description": "Текущий параметр температуры"
},
"max_tokens": {
"name": "Лимит токенов",
"description": "Текущий лимит максимальных токенов"
},
"total_responses": {
"name": "Всего ответов",
"description": "Количество ответов с последнего сброса"
},
"system_prompt": {
"name": "Системный промпт",
"description": "Текущие системные инструкции для AI"
},
"response_time": {
"name": "Время ответа",
"description": "Время генерации последнего ответа"
},
"queue_size": {
"name": "Размер очереди",
"description": "Текущий размер очереди запросов"
},
"api_status": {
"name": "Статус API",
"description": "Текущий статус подключения к API"
},
"error_count": {
"name": "Счётчик ошибок",
"description": "Количество ошибок с последнего сброса"
},
"last_error": {
"name": "Последняя ошибка",
"description": "Описание последней возникшей ошибки"
}
}
}
}
},
"services": {
"ask_question": {
"name": "Задать вопрос",
"description": "Отправить вопрос модели AI и получить подробный ответ. Ответ сохраняется в истории.",
"fields": {
"question": {
"name": "Вопрос",
"description": "Ваш вопрос или запрос для AI. Будьте конкретны для лучших результатов."
},
"model": {
"name": "Модель",
"description": "Модель AI для использования (необязательно, переопределяет настройки по умолчанию)."
},
"temperature": {
"name": "Температура",
"description": "Уровень креативности ответа (0-2, необязательно)."
},
"max_tokens": {
"name": "Максимум токенов",
"description": "Максимальная длина ответа (необязательно)."
}
}
},
"clear_history": {
"name": "Очистить историю",
"description": "Удалить всю историю разговоров. Это действие нельзя отменить."
},
"get_history": {
"name": "Получить историю",
"description": "Получить историю разговоров, включая вопросы, ответы и временные метки.",
"fields": {
"limit": {
"name": "Лимит",
"description": "Количество последних разговоров для получения (по умолчанию 10)."
},
"filter_model": {
"name": "Фильтр по модели",
"description": "Получить только разговоры с определённой моделью AI."
}
}
},
"set_system_prompt": {
"name": "Установить системный промпт",
"description": "Настроить поведение AI, установив системный промпт.",
"fields": {
"prompt": {
"name": "Промпт",
"description": "Инструкции, определяющие поведение и стиль ответов AI."
},
"clear_prompt": {
"name": "Очистить промпт",
"description": "Удалить текущий системный промпт перед установкой нового."
}
}
}
}
}
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -4,6 +4,6 @@
"domains": ["sensor"], "domains": ["sensor"],
"homeassistant": "2024.11.0", "homeassistant": "2024.11.0",
"icon": "mdi:brain", "icon": "mdi:brain",
"version": "1.0.5", "version": "2.0.0",
"documentation": "https://github.com/smkrv/ha-text-ai" "documentation": "https://github.com/smkrv/ha-text-ai"
} }
+17
View File
@@ -0,0 +1,17 @@
```
ha-text-ai/
├── custom_components/
│ └── ha_text_ai/
│ ├── __init__.py
│ ├── config_flow.py
│ ├── coordinator.py
│ ├── manifest.json
│ ├── sensor.py
│ ├── services.yaml
│ └── const.py
└── strings/
├── en.json
└── ru.json
```