mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-26 00:54:00 +08:00
Compare commits
62
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d064f5b9d | ||
|
|
ae8f8bda03 | ||
|
|
2dbae19c41 | ||
|
|
d3ef31f551 | ||
|
|
ff0c0369e8 | ||
|
|
b0dafe081b | ||
|
|
a88b5d01c1 | ||
|
|
e7d5e62671 | ||
|
|
5f41b9489d | ||
|
|
958e241e0e | ||
|
|
4b3efc0b6f | ||
|
|
1592fc2371 | ||
|
|
d5a6613428 | ||
|
|
9324473c9e | ||
|
|
456e797cca | ||
|
|
f960b9f9b4 | ||
|
|
8449b73423 | ||
|
|
f19ba9aac9 | ||
|
|
524849f6a9 | ||
|
|
0a64a9abe0 | ||
|
|
adb4127f5a | ||
|
|
f463593180 | ||
|
|
e53f257977 | ||
|
|
87199e856a | ||
|
|
bcf7cfbf76 | ||
|
|
9289e1388f | ||
|
|
84db9b7bb8 | ||
|
|
bd1098a181 | ||
|
|
6fd3db0063 | ||
|
|
bf52217cd5 | ||
|
|
b0cf5b3c61 | ||
|
|
347c1675ca | ||
|
|
961ae2d34f | ||
|
|
14827c3adc | ||
|
|
4a88453abc | ||
|
|
bc33d38f5e | ||
|
|
657a5ede86 | ||
|
|
dd046fe3e8 | ||
|
|
7391dbf5b1 | ||
|
|
a0ccb86bd4 | ||
|
|
c0eb5b165d | ||
|
|
9812babc7e | ||
|
|
c61c4570b0 | ||
|
|
d618feffff | ||
|
|
0fdc3c93d3 | ||
|
|
d6e76f7805 | ||
|
|
b05afe1085 | ||
|
|
2a4911f5f8 | ||
|
|
208074d845 | ||
|
|
616ff2c3fe | ||
|
|
987c939956 | ||
|
|
1615cc744e | ||
|
|
2d793a4b25 | ||
|
|
aeeb4d5504 | ||
|
|
daec801073 | ||
|
|
e4916a7b7c | ||
|
|
a08abd76e3 | ||
|
|
a2be709608 | ||
|
|
69002ef926 | ||
|
|
9ade5a7194 | ||
|
|
9b3f4f605b | ||
|
|
b862968d01 |
@@ -0,0 +1,17 @@
|
|||||||
|
name: Validate
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-hacs:
|
||||||
|
runs-on: "ubuntu-latest"
|
||||||
|
steps:
|
||||||
|
- name: HACS validation
|
||||||
|
uses: "hacs/action@main"
|
||||||
|
with:
|
||||||
|
category: "integration"
|
||||||
@@ -37,3 +37,4 @@ wheels/
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
*.psd
|
*.psd
|
||||||
*.zip
|
*.zip
|
||||||
|
*.txt
|
||||||
|
|||||||
@@ -2,22 +2,26 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
    [](https://opensource.org/licenses/MIT) [](https://github.com/hacs/integration)
|
  [](https://opensource.org/licenses/MIT) [](https://github.com/hacs/integration)   
|
||||||
|
|
||||||
<img src="https://github.com/smkrv/ha-text-ai/blob/3e3ec45b195c92989434fde40ae110027f4ea124/misc/icons/icon.png" alt="HA Text AI" width="140"/>
|
<img src="https://github.com/smkrv/ha-text-ai/blob/524849f6a945ec62c2cf6a6b7ecd9a28b37bf0fa/misc/icons/logo.jpg" alt="HA Text AI" height="160"/>
|
||||||
|
|
||||||
### Advanced AI Integration for Home Assistant with multi-provider support
|
### Advanced AI Integration for Home Assistant with LLM multi-provider support
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
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.
|
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>
|
||||||
|
|
||||||
---
|
---
|
||||||
> [!NOTE]
|
|
||||||
|
> [!IMPORTANT]
|
||||||
> 🚧 ALPHA VERSION 🚧
|
> 🚧 ALPHA VERSION 🚧
|
||||||
> Expect: potential bugs, frequent changes, incomplete features.
|
> Expect: potential bugs, frequent changes, incomplete features.
|
||||||
> 🤝 Community Driven
|
> 🤝 Community Driven
|
||||||
|
>
|
||||||
|
> <a href="https://community.home-assistant.io/t/ha-text-ai-transforming-home-automation-with-multi-provider-language-models/799741"><img src="https://img.shields.io/badge/Community-blue?style=for-the-badge&logo=homeassistant&logoColor=white&color=03a9f4"/></a>
|
||||||
|
|
||||||
## 🌟 Features
|
## 🌟 Features
|
||||||
|
|
||||||
@@ -71,7 +75,7 @@ Transform your smart home experience with powerful AI assistance powered by mult
|
|||||||
|
|
||||||
## 📋 Prerequisites
|
## 📋 Prerequisites
|
||||||
|
|
||||||
- Home Assistant 2023.11 or later
|
- Home Assistant 2024.11 or later
|
||||||
- Active API key from:
|
- Active API key from:
|
||||||
- OpenAI ([Get key](https://platform.openai.com/account/api-keys))
|
- OpenAI ([Get key](https://platform.openai.com/account/api-keys))
|
||||||
- Anthropic ([Get key](https://console.anthropic.com/))
|
- Anthropic ([Get key](https://console.anthropic.com/))
|
||||||
@@ -139,20 +143,61 @@ To be compatible, a provider should support:
|
|||||||
4. Follow the configuration steps
|
4. Follow the configuration steps
|
||||||
|
|
||||||
### Via YAML
|
### Via YAML
|
||||||
|
|
||||||
|
### Platform Configuration (Global Settings)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
ha_text_ai:
|
ha_text_ai:
|
||||||
api_provider: openai # or anthropic
|
api_provider: openai # Required
|
||||||
api_key: !secret ai_api_key
|
api_key: !secret ai_api_key # Required
|
||||||
model: gpt-4o-mini
|
model: gpt-4o-mini # Strongly recommended
|
||||||
temperature: 0.7
|
temperature: 0.7 # Optional
|
||||||
max_tokens: 1000
|
max_tokens: 1000 # Optional
|
||||||
request_interval: 1.0
|
request_interval: 1.0 # Optional
|
||||||
api_endpoint: https://api.openai.com/v1 # optional, for custom endpoints
|
api_endpoint: https://api.openai.com/v1 # Required
|
||||||
system_prompt: |
|
system_prompt: | # Optional
|
||||||
You are a home automation expert assistant.
|
You are a home automation expert assistant.
|
||||||
Focus on practical and efficient solutions.
|
Focus on practical and efficient solutions.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Sensor Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
sensor:
|
||||||
|
- platform: ha_text_ai
|
||||||
|
name: "My AI Assistant" # Required, unique identifier
|
||||||
|
api_provider: openai # Optional (inherits from platform)
|
||||||
|
model: "gpt-4o-mini" # Optional
|
||||||
|
temperature: 0.7 # Optional
|
||||||
|
max_tokens: 1000 # Optional
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📋 Configuration Parameters
|
||||||
|
|
||||||
|
#### Platform Configuration
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Default | Description |
|
||||||
|
|-----------|------|----------|---------|-------------|
|
||||||
|
| `api_provider` | String | ✅ | - | AI service provider (openai, anthropic) |
|
||||||
|
| `api_key` | String | ✅ | - | Authentication key for AI service |
|
||||||
|
| `model` | String | ⚠️ | Provider default | Strongly recommended: Specific AI model to use. If not specified, the provider's default model will be used |
|
||||||
|
| `temperature` | Float | ❌ | 0.7 | Response creativity level (0.0-2.0) |
|
||||||
|
| `max_tokens` | Integer | ❌ | 1000 | Maximum response length |
|
||||||
|
| `request_interval` | Float | ❌ | 1.0 | Delay between API requests |
|
||||||
|
| `api_endpoint` | URL | ⚠️ | Provider default | Custom API endpoint |
|
||||||
|
| `system_prompt` | String | ❌ | - | Default context for AI interactions |
|
||||||
|
|
||||||
|
#### Sensor Configuration
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Default | Description |
|
||||||
|
|-----------|------|----------|---------|-------------|
|
||||||
|
| `platform` | String | ✅ | - | Must be `ha_text_ai` |
|
||||||
|
| `name` | String | ✅ | - | Unique sensor identifier |
|
||||||
|
| `api_provider` | String | ❌ | Platform setting | Override global provider |
|
||||||
|
| `model` | String | ⚠️ | Platform setting | Recommended: Override global model. If not specified, uses platform or provider default |
|
||||||
|
| `temperature` | Float | ❌ | Platform setting | Override global temperature |
|
||||||
|
| `max_tokens` | Integer | ❌ | Platform setting | Override global max tokens |
|
||||||
|
|
||||||
## 🛠️ Available Services
|
## 🛠️ Available Services
|
||||||
|
|
||||||
### ask_question
|
### ask_question
|
||||||
@@ -192,6 +237,143 @@ data:
|
|||||||
filter_model: "gpt-4o" # optional
|
filter_model: "gpt-4o" # optional
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 🏷️ HA Text AI Sensor Naming Convention
|
||||||
|
|
||||||
|
#### Character Restrictions
|
||||||
|
- Only lowercase letters (a-z)
|
||||||
|
- Numbers (0-9)
|
||||||
|
- Underscore (_)
|
||||||
|
- Maximum length: 50 characters (including `ha_text_ai_` prefix (14 characters)
|
||||||
|
|
||||||
|
#### Sensor Name Structure
|
||||||
|
```yaml
|
||||||
|
# Always starts with 'sensor.ha_text_ai_'
|
||||||
|
# You define only the part after the underscore
|
||||||
|
sensor.ha_text_ai_YOUR_UNIQUE_SUFFIX
|
||||||
|
|
||||||
|
# Examples:
|
||||||
|
sensor.ha_text_ai_gpt # GPT-based sensor
|
||||||
|
sensor.ha_text_ai_claude # Claude-based sensor
|
||||||
|
sensor.ha_text_ai_gpt # Custom suffix
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response Retrieval
|
||||||
|
```yaml
|
||||||
|
# Use your specific sensor name
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'response') }}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Practical Usage
|
||||||
|
```yaml
|
||||||
|
automation:
|
||||||
|
- alias: "AI Response with Custom Sensor"
|
||||||
|
action:
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: "Home automation advice"
|
||||||
|
- service: notify.mobile
|
||||||
|
data:
|
||||||
|
message: >
|
||||||
|
AI Tip:
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'response') }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 💡 Naming Rules
|
||||||
|
- Prefix is always `sensor.ha_text_ai_`
|
||||||
|
- Add your unique identifier after the underscore
|
||||||
|
- Use lowercase
|
||||||
|
- No spaces allowed
|
||||||
|
- Keep it descriptive but concise
|
||||||
|
|
||||||
|
### 🔍 HA Text AI Sensor Attributes
|
||||||
|
|
||||||
|
#### Model and Provider Information
|
||||||
|
```yaml
|
||||||
|
# Name of the AI model currently in use (e.g., latest version of GPT)
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Model') }} # gpt-4o
|
||||||
|
|
||||||
|
# Service provider for the AI model (determines API endpoint and authentication)
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Api provider') }} # openai
|
||||||
|
|
||||||
|
# Previous or alternative model configuration
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Last model') }} # gpt-4o
|
||||||
|
```
|
||||||
|
|
||||||
|
#### System Status
|
||||||
|
```yaml
|
||||||
|
# Current operational readiness of the AI service API
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Api status') }} # ready
|
||||||
|
|
||||||
|
# Indicates if a request is currently being processed
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Is processing') }} # false
|
||||||
|
|
||||||
|
# Shows if the API has hit its request rate limit
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Is rate limited') }} # false
|
||||||
|
|
||||||
|
# Status of the specific API endpoint being used
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Endpoint status') }} # ready
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Performance Metrics
|
||||||
|
```yaml
|
||||||
|
# Total number of successfully completed API requests
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Successful requests') }} # 0
|
||||||
|
|
||||||
|
# Number of API requests that encountered errors
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Failed requests') }} # 0
|
||||||
|
|
||||||
|
# Mean time taken to receive a response from the AI service
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Average latency') }} # 0
|
||||||
|
|
||||||
|
# Maximum time taken for a single request-response cycle
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Max latency') }} # 0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Conversation and Token Usage
|
||||||
|
```yaml
|
||||||
|
# Number of previous interactions stored in conversation context
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'History size') }} # 0
|
||||||
|
|
||||||
|
# Total number of tokens used across all interactions
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Total tokens') }} # 0
|
||||||
|
|
||||||
|
# Tokens used in the input prompts
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Prompt tokens') }} # 0
|
||||||
|
|
||||||
|
# Tokens used in the AI's generated responses
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Completion tokens') }} # 0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Last Interaction Details
|
||||||
|
```yaml
|
||||||
|
# Most recent complete response generated by the AI service
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Response') }} # Last AI response
|
||||||
|
|
||||||
|
# The most recently processed user query or prompt
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Question') }} # Last asked question
|
||||||
|
|
||||||
|
# Precise moment when the last interaction occurred (useful for tracking and logging)
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Last timestamp') }} # Timestamp
|
||||||
|
```
|
||||||
|
|
||||||
|
#### System Health
|
||||||
|
```yaml
|
||||||
|
# Cumulative count of all errors encountered during AI service interactions
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Total errors') }} # 0
|
||||||
|
|
||||||
|
# Indicates if the AI service is currently undergoing scheduled or emergency maintenance
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Is maintenance') }} # false
|
||||||
|
|
||||||
|
# Total continuous operational time of the AI service (in hours or days)
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Uptime') }} # 547,58
|
||||||
|
```
|
||||||
|
|
||||||
|
### 💡 Pro Tips
|
||||||
|
- Always check attribute existence
|
||||||
|
- Use these attributes for monitoring and automation
|
||||||
|
- Some values might be 0 or empty initially
|
||||||
|
|
||||||
|
|
||||||
## 📘 FAQ
|
## 📘 FAQ
|
||||||
|
|
||||||
**Q: Which AI providers are supported?**
|
**Q: Which AI providers are supported?**
|
||||||
@@ -232,11 +414,27 @@ Contributions welcome! Please read our [Contributing Guide](CONTRIBUTING.md).
|
|||||||
|
|
||||||
MIT License - see [LICENSE](LICENSE) for details.
|
MIT License - see [LICENSE](LICENSE) for details.
|
||||||
|
|
||||||
|
## 💡 Support the Project
|
||||||
|
|
||||||
|
The best support is:
|
||||||
|
- Sharing feedback
|
||||||
|
- Contributing ideas
|
||||||
|
- Recommending to friends
|
||||||
|
- Reporting issues
|
||||||
|
- Star the repository
|
||||||
|
|
||||||
|
If you want to say thanks financially, you can send a small token of appreciation in USDT:
|
||||||
|
|
||||||
|
**USDT Wallet (TRC10/TRC20):**
|
||||||
|
`TXC9zYHYPfWUGi4Sv4R1ctTBGScXXQk5HZ`
|
||||||
|
|
||||||
|
*Open-source is built by community passion!* 🚀
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
Made with ❤️ for the Home Assistant Community
|
Made with ❤️ and Claude 3.5 Sonnet for the Home Assistant Community
|
||||||
|
|
||||||
[Report Bug](https://github.com/smkrv/ha-text-ai/issues) · [Request Feature](https://github.com/smkrv/ha-text-ai/issues)
|
[Report Bug](https://github.com/smkrv/ha-text-ai/issues) · [Request Feature](https://github.com/smkrv/ha-text-ai/issues)
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ from .const import (
|
|||||||
SERVICE_CLEAR_HISTORY,
|
SERVICE_CLEAR_HISTORY,
|
||||||
SERVICE_GET_HISTORY,
|
SERVICE_GET_HISTORY,
|
||||||
SERVICE_SET_SYSTEM_PROMPT,
|
SERVICE_SET_SYSTEM_PROMPT,
|
||||||
|
DEFAULT_MAX_HISTORY,
|
||||||
|
CONF_MAX_HISTORY_SIZE,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -89,7 +91,7 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
|
|||||||
source = os.path.join(os.path.dirname(__file__), 'icons', 'icon.svg')
|
source = os.path.join(os.path.dirname(__file__), 'icons', 'icon.svg')
|
||||||
dest_dir = os.path.join(hass.config.path('www'), 'icons')
|
dest_dir = os.path.join(hass.config.path('www'), 'icons')
|
||||||
os.makedirs(dest_dir, exist_ok=True)
|
os.makedirs(dest_dir, exist_ok=True)
|
||||||
dest = os.path.join(dest_dir, 'icon.svg')
|
dest = os.path.join(dest_dir, 'icon.png')
|
||||||
if not os.path.exists(dest):
|
if not os.path.exists(dest):
|
||||||
shutil.copyfile(source, dest)
|
shutil.copyfile(source, dest)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -197,11 +199,14 @@ async def async_check_api(session, endpoint: str, headers: dict, provider: str)
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up HA Text AI from a config entry."""
|
"""Set up HA Text AI from a config entry."""
|
||||||
|
_LOGGER.debug(f"Setting up HA Text AI entry: {entry.data}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if CONF_API_PROVIDER not in entry.data:
|
if CONF_API_PROVIDER not in entry.data:
|
||||||
_LOGGER.error("API provider not specified")
|
_LOGGER.error("API provider not specified")
|
||||||
raise ConfigEntryNotReady("API provider is required")
|
raise ConfigEntryNotReady("API provider is required")
|
||||||
|
|
||||||
|
# Get configuration
|
||||||
session = aiohttp_client.async_get_clientsession(hass)
|
session = aiohttp_client.async_get_clientsession(hass)
|
||||||
api_provider = entry.data.get(CONF_API_PROVIDER)
|
api_provider = entry.data.get(CONF_API_PROVIDER)
|
||||||
model = entry.data.get(CONF_MODEL, DEFAULT_MODEL)
|
model = entry.data.get(CONF_MODEL, DEFAULT_MODEL)
|
||||||
@@ -212,6 +217,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
).rstrip('/')
|
).rstrip('/')
|
||||||
api_key = entry.data[CONF_API_KEY]
|
api_key = entry.data[CONF_API_KEY]
|
||||||
instance_name = entry.data.get(CONF_NAME, entry.entry_id)
|
instance_name = entry.data.get(CONF_NAME, entry.entry_id)
|
||||||
|
request_interval = entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)
|
||||||
|
max_tokens = entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)
|
||||||
|
temperature = entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
|
||||||
|
max_history_size = entry.data.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY)
|
||||||
|
context_messages = entry.data.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES)
|
||||||
is_anthropic = api_provider == API_PROVIDER_ANTHROPIC
|
is_anthropic = api_provider == API_PROVIDER_ANTHROPIC
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
@@ -242,39 +252,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
hass=hass,
|
hass=hass,
|
||||||
client=api_client,
|
client=api_client,
|
||||||
model=model,
|
model=model,
|
||||||
update_interval=entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
|
update_interval=request_interval,
|
||||||
instance_name=instance_name,
|
instance_name=instance_name,
|
||||||
max_tokens=entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
|
max_tokens=max_tokens,
|
||||||
temperature=entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
|
temperature=temperature,
|
||||||
|
max_history_size=max_history_size,
|
||||||
|
context_messages=context_messages,
|
||||||
is_anthropic=is_anthropic,
|
is_anthropic=is_anthropic,
|
||||||
context_messages=entry.data.get(
|
|
||||||
CONF_CONTEXT_MESSAGES,
|
|
||||||
DEFAULT_CONTEXT_MESSAGES
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
coordinator.data = coordinator._initial_state.copy()
|
_LOGGER.debug(f"Created coordinator for {instance_name}")
|
||||||
_LOGGER.debug(f"Initial state set for coordinator {instance_name}")
|
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
|
||||||
|
|
||||||
|
# Store coordinator
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||||
|
|
||||||
|
_LOGGER.debug(f"Stored coordinator in hass.data[{DOMAIN}][{entry.entry_id}]")
|
||||||
|
|
||||||
|
# Set up platforms
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.debug(f"Setup completed for {instance_name}")
|
||||||
"Successfully set up %s instance '%s' with model %s",
|
|
||||||
api_provider,
|
|
||||||
instance_name,
|
|
||||||
model
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as err:
|
||||||
_LOGGER.exception("Setup error: %s", str(ex))
|
_LOGGER.exception(f"Error setting up HA Text AI: {err}")
|
||||||
raise ConfigEntryNotReady(f"Setup error: {str(ex)}") from ex
|
raise
|
||||||
|
|
||||||
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."""
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from .const import (
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class APIClient:
|
class APIClient:
|
||||||
"""API Client for OpenAI and Anthropic."""
|
"""API Client for OpenAI and Anthropic."""
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ class APIClient:
|
|||||||
payload: Dict[str, Any],
|
payload: Dict[str, Any],
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Make API request with retry logic."""
|
"""Make API request with retry logic."""
|
||||||
|
_LOGGER.debug(f"API Request: URL={url}, Payload={payload}")
|
||||||
for attempt in range(API_RETRY_COUNT):
|
for attempt in range(API_RETRY_COUNT):
|
||||||
try:
|
try:
|
||||||
async with timeout(API_TIMEOUT):
|
async with timeout(API_TIMEOUT):
|
||||||
@@ -66,20 +68,23 @@ class APIClient:
|
|||||||
url,
|
url,
|
||||||
json=payload,
|
json=payload,
|
||||||
headers=self.headers,
|
headers=self.headers,
|
||||||
timeout=self.timeout
|
timeout=self.timeout,
|
||||||
) as response:
|
) as response:
|
||||||
|
_LOGGER.debug(f"Response status: {response.status}")
|
||||||
if response.status != 200:
|
if response.status != 200:
|
||||||
error_data = await response.json()
|
error_data = await response.json()
|
||||||
|
_LOGGER.error(f"API error: {error_data}")
|
||||||
raise HomeAssistantError(f"API error: {error_data}")
|
raise HomeAssistantError(f"API error: {error_data}")
|
||||||
return await response.json()
|
return await response.json()
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.warning(f"Timeout on attempt {attempt + 1}")
|
||||||
if attempt == API_RETRY_COUNT - 1:
|
if attempt == API_RETRY_COUNT - 1:
|
||||||
raise HomeAssistantError("API request timed out")
|
raise HomeAssistantError("API request timed out")
|
||||||
await asyncio.sleep(1 * (attempt + 1))
|
await asyncio.sleep(1 * (attempt + 1))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
_LOGGER.warning(f"API request failed on attempt {attempt + 1}: {str(e)}")
|
||||||
if attempt == API_RETRY_COUNT - 1:
|
if attempt == API_RETRY_COUNT - 1:
|
||||||
raise
|
raise
|
||||||
_LOGGER.warning("API request failed, retrying: %s", str(e))
|
|
||||||
await asyncio.sleep(1 * (attempt + 1))
|
await asyncio.sleep(1 * (attempt + 1))
|
||||||
|
|
||||||
async def create(
|
async def create(
|
||||||
@@ -101,6 +106,11 @@ class APIClient:
|
|||||||
return await self._create_openai_completion(
|
return await self._create_openai_completion(
|
||||||
model, messages, temperature, max_tokens
|
model, messages, temperature, max_tokens
|
||||||
)
|
)
|
||||||
|
except (KeyError, IndexError) as e:
|
||||||
|
if "'choices'" in str(e) or "'message'" in str(e):
|
||||||
|
raise HomeAssistantError("Failed to get a response from the AI model. Please check your internet connection and try again later.")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_LOGGER.error("API request failed: %s", str(e))
|
_LOGGER.error("API request failed: %s", str(e))
|
||||||
raise HomeAssistantError(f"API request failed: {str(e)}")
|
raise HomeAssistantError(f"API request failed: {str(e)}")
|
||||||
@@ -125,16 +135,14 @@ class APIClient:
|
|||||||
return {
|
return {
|
||||||
"choices": [
|
"choices": [
|
||||||
{
|
{
|
||||||
"message": {
|
"message": {"content": data["choices"][0]["message"]["content"]},
|
||||||
"content": data["choices"][0]["message"]["content"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"usage": {
|
"usage": {
|
||||||
"prompt_tokens": data["usage"]["prompt_tokens"],
|
"prompt_tokens": data["usage"]["prompt_tokens"],
|
||||||
"completion_tokens": data["usage"]["completion_tokens"],
|
"completion_tokens": data["usage"]["completion_tokens"],
|
||||||
"total_tokens": data["usage"]["total_tokens"]
|
"total_tokens": data["usage"]["total_tokens"],
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _create_anthropic_completion(
|
async def _create_anthropic_completion(
|
||||||
@@ -147,19 +155,24 @@ class APIClient:
|
|||||||
"""Create completion using Anthropic API."""
|
"""Create completion using Anthropic API."""
|
||||||
url = f"{self.endpoint}/v1/messages"
|
url = f"{self.endpoint}/v1/messages"
|
||||||
|
|
||||||
# Convert messages to Anthropic format
|
system_prompt = None
|
||||||
system_prompt = next(
|
filtered_messages = []
|
||||||
(msg["content"] for msg in messages if msg["role"] == "system"),
|
for msg in messages:
|
||||||
None
|
if msg['role'] == 'system':
|
||||||
)
|
if system_prompt is None:
|
||||||
conversation = [msg for msg in messages if msg["role"] != "system"]
|
system_prompt = msg['content']
|
||||||
|
else:
|
||||||
|
system_prompt += f" {msg['content']}"
|
||||||
|
else:
|
||||||
|
filtered_messages.append(msg)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": conversation,
|
"messages": filtered_messages,
|
||||||
"max_tokens": max_tokens,
|
"max_tokens": max_tokens,
|
||||||
"temperature": temperature,
|
"temperature": temperature,
|
||||||
}
|
}
|
||||||
|
|
||||||
if system_prompt:
|
if system_prompt:
|
||||||
payload["system"] = system_prompt
|
payload["system"] = system_prompt
|
||||||
|
|
||||||
@@ -167,14 +180,26 @@ class APIClient:
|
|||||||
return {
|
return {
|
||||||
"choices": [
|
"choices": [
|
||||||
{
|
{
|
||||||
"message": {
|
"message": {"content": data["content"][0]["text"]},
|
||||||
"content": data["content"][0]["text"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"usage": {
|
"usage": {
|
||||||
"prompt_tokens": data["usage"]["input_tokens"],
|
"prompt_tokens": data["usage"]["input_tokens"],
|
||||||
"completion_tokens": data["usage"]["output_tokens"],
|
"completion_tokens": data["usage"]["output_tokens"],
|
||||||
"total_tokens": data["usage"]["input_tokens"] + data["usage"]["output_tokens"]
|
"total_tokens": data["usage"]["input_tokens"] + data["usage"]["output_tokens"],
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def check_connection(self) -> bool:
|
||||||
|
"""Check API connection."""
|
||||||
|
try:
|
||||||
|
await self._make_request(self.endpoint, {"test": "connection"})
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(f"Connection check failed: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def shutdown(self) -> None:
|
||||||
|
"""Shutdown API client."""
|
||||||
|
_LOGGER.debug("Shutting down API client")
|
||||||
|
await self.session.close()
|
||||||
|
|||||||
@@ -34,10 +34,21 @@ from .const import (
|
|||||||
MIN_MAX_TOKENS,
|
MIN_MAX_TOKENS,
|
||||||
MAX_MAX_TOKENS,
|
MAX_MAX_TOKENS,
|
||||||
MIN_REQUEST_INTERVAL,
|
MIN_REQUEST_INTERVAL,
|
||||||
|
DEFAULT_NAME_PREFIX,
|
||||||
|
DEFAULT_MAX_HISTORY,
|
||||||
|
CONF_MAX_HISTORY_SIZE,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_name(name: str) -> str:
|
||||||
|
"""Normalize name to conform to HA naming convention using underscores."""
|
||||||
|
normalized = ''.join(c if c.isalnum() or c == '_' else '_' for c in name)
|
||||||
|
normalized = '_'.join(filter(None, normalized.split('_')))
|
||||||
|
return normalized.lower()
|
||||||
|
|
||||||
|
|
||||||
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."""
|
||||||
|
|
||||||
@@ -69,18 +80,18 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
async def async_step_provider(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
async def async_step_provider(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
||||||
"""Handle provider configuration step."""
|
"""Handle provider configuration step."""
|
||||||
|
self._errors = {}
|
||||||
|
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
default_endpoint = (
|
default_endpoint = (
|
||||||
DEFAULT_OPENAI_ENDPOINT if self._provider == API_PROVIDER_OPENAI
|
DEFAULT_OPENAI_ENDPOINT if self._provider == API_PROVIDER_OPENAI
|
||||||
else DEFAULT_ANTHROPIC_ENDPOINT
|
else DEFAULT_ANTHROPIC_ENDPOINT
|
||||||
)
|
)
|
||||||
|
|
||||||
suggested_name = f"HA Text AI {len(self._async_current_entries()) + 1}"
|
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="provider",
|
step_id="provider",
|
||||||
data_schema=vol.Schema({
|
data_schema=vol.Schema({
|
||||||
vol.Required(CONF_NAME, default=suggested_name): str,
|
vol.Required(CONF_NAME, default="my_assistant"): str,
|
||||||
vol.Required(CONF_API_KEY): str,
|
vol.Required(CONF_API_KEY): str,
|
||||||
vol.Required(CONF_MODEL, default=DEFAULT_MODEL): str,
|
vol.Required(CONF_MODEL, default=DEFAULT_MODEL): str,
|
||||||
vol.Required(CONF_API_ENDPOINT, default=default_endpoint): str,
|
vol.Required(CONF_API_ENDPOINT, default=default_endpoint): str,
|
||||||
@@ -103,27 +114,86 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
vol.Coerce(int),
|
vol.Coerce(int),
|
||||||
vol.Range(min=1, max=20)
|
vol.Range(min=1, max=20)
|
||||||
),
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MAX_HISTORY_SIZE,
|
||||||
|
default=DEFAULT_MAX_HISTORY
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=100)
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
input_copy = user_input.copy()
|
||||||
|
|
||||||
|
try:
|
||||||
|
normalized_name = self._validate_and_normalize_name(input_copy[CONF_NAME])
|
||||||
|
input_copy[CONF_NAME] = normalized_name
|
||||||
|
except ValueError as e:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="provider",
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
vol.Required(CONF_NAME, default=input_copy[CONF_NAME]): str,
|
||||||
|
vol.Required(CONF_API_KEY, default=input_copy[CONF_API_KEY]): str,
|
||||||
|
vol.Required(CONF_MODEL, default=input_copy[CONF_MODEL]): str,
|
||||||
|
vol.Required(CONF_API_ENDPOINT, default=input_copy[CONF_API_ENDPOINT]): str,
|
||||||
|
vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
errors={"name": str(e)}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not await self._async_validate_api(input_copy):
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="provider",
|
||||||
|
data_schema=vol.Schema({
|
||||||
}),
|
}),
|
||||||
errors=self._errors
|
errors=self._errors
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="provider",
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
}),
|
||||||
|
errors={"base": str(e)}
|
||||||
|
)
|
||||||
|
|
||||||
instance_name = user_input[CONF_NAME]
|
return await self._create_entry(input_copy)
|
||||||
await self._async_validate_name(instance_name)
|
|
||||||
if self._errors:
|
|
||||||
return await self.async_step_provider()
|
|
||||||
|
|
||||||
if not await self._async_validate_api(user_input):
|
def _validate_and_normalize_name(self, name: str) -> str:
|
||||||
return await self.async_step_provider()
|
"""
|
||||||
|
Validate and normalize name with detailed error handling.
|
||||||
|
|
||||||
return await self._create_entry(user_input)
|
Raises:
|
||||||
|
ValueError: If name is invalid
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Normalized name
|
||||||
|
"""
|
||||||
|
if not name:
|
||||||
|
raise ValueError("empty")
|
||||||
|
|
||||||
|
name = name.strip()
|
||||||
|
normalized = ''.join(
|
||||||
|
c if c.isalnum() or c in ' _' else '_' # Only allow underscores
|
||||||
|
for c in name
|
||||||
|
)
|
||||||
|
|
||||||
|
normalized = normalized.replace(' ', '_').lower()
|
||||||
|
|
||||||
async def _async_validate_name(self, name: str) -> bool:
|
|
||||||
"""Validate that the name is unique."""
|
|
||||||
for entry in self._async_current_entries():
|
for entry in self._async_current_entries():
|
||||||
if entry.data.get(CONF_NAME) == name:
|
if entry.data.get(CONF_NAME, "") == normalized:
|
||||||
self._errors["name"] = "name_exists"
|
raise ValueError("name_exists")
|
||||||
return False
|
|
||||||
return True
|
normalized = normalized[:50]
|
||||||
|
|
||||||
|
if not normalized:
|
||||||
|
raise ValueError("empty")
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
|
||||||
async def _async_validate_api(self, user_input: Dict[str, Any]) -> bool:
|
async def _async_validate_api(self, user_input: Dict[str, Any]) -> bool:
|
||||||
"""Validate API connection."""
|
"""Validate API connection."""
|
||||||
@@ -167,21 +237,36 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def _create_entry(self, user_input: Dict[str, Any]) -> FlowResult:
|
async def _create_entry(self, user_input: Dict[str, Any]) -> FlowResult:
|
||||||
"""Create the config entry."""
|
"""Create the config entry with comprehensive data preservation."""
|
||||||
instance_name = user_input[CONF_NAME]
|
instance_name = user_input[CONF_NAME]
|
||||||
unique_id = f"{DOMAIN}_{instance_name}_{self._provider}".lower().replace(" ", "_")
|
normalized_name = normalize_name(instance_name)
|
||||||
|
|
||||||
|
unique_id = f"{DOMAIN}_{normalized_name}_{self._provider}".lower()
|
||||||
|
|
||||||
|
entry_data = {
|
||||||
|
CONF_API_PROVIDER: self._provider,
|
||||||
|
CONF_NAME: instance_name,
|
||||||
|
"normalized_name": normalized_name,
|
||||||
|
CONF_API_KEY: user_input.get(CONF_API_KEY),
|
||||||
|
CONF_API_ENDPOINT: user_input.get(CONF_API_ENDPOINT),
|
||||||
|
"unique_id": unique_id,
|
||||||
|
CONF_MODEL: user_input.get(CONF_MODEL, DEFAULT_MODEL),
|
||||||
|
CONF_TEMPERATURE: user_input.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
|
||||||
|
CONF_MAX_TOKENS: user_input.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
|
||||||
|
CONF_REQUEST_INTERVAL: user_input.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
|
||||||
|
CONF_CONTEXT_MESSAGES: user_input.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES),
|
||||||
|
CONF_MAX_HISTORY_SIZE: user_input.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY),
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in user_input.items():
|
||||||
|
if key not in entry_data:
|
||||||
|
entry_data[key] = value
|
||||||
|
|
||||||
|
_LOGGER.debug(f"Creating config entry with data: {entry_data}")
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=instance_name,
|
title=instance_name,
|
||||||
data={
|
data=entry_data
|
||||||
CONF_API_PROVIDER: self._provider,
|
|
||||||
CONF_NAME: instance_name,
|
|
||||||
**user_input,
|
|
||||||
"unique_id": unique_id,
|
|
||||||
CONF_CONTEXT_MESSAGES: user_input.get(
|
|
||||||
CONF_CONTEXT_MESSAGES,
|
|
||||||
DEFAULT_CONTEXT_MESSAGES)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -243,5 +328,15 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
vol.Coerce(int),
|
vol.Coerce(int),
|
||||||
vol.Range(min=1, max=20)
|
vol.Range(min=1, max=20)
|
||||||
),
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MAX_HISTORY_SIZE,
|
||||||
|
default=current_data.get(
|
||||||
|
CONF_MAX_HISTORY_SIZE,
|
||||||
|
DEFAULT_MAX_HISTORY
|
||||||
|
)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=100)
|
||||||
|
),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from homeassistant.helpers import config_validation as cv
|
|||||||
|
|
||||||
# Domain and platforms
|
# Domain and platforms
|
||||||
DOMAIN: Final = "ha_text_ai"
|
DOMAIN: Final = "ha_text_ai"
|
||||||
PLATFORMS: Final = [Platform.SENSOR]
|
PLATFORMS: list[str] = ["sensor"]
|
||||||
|
|
||||||
# Provider configuration
|
# Provider configuration
|
||||||
CONF_API_PROVIDER: Final = "api_provider"
|
CONF_API_PROVIDER: Final = "api_provider"
|
||||||
@@ -29,7 +29,7 @@ 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"
|
||||||
CONF_INSTANCE: Final = "instance"
|
CONF_INSTANCE: Final = "instance"
|
||||||
CONF_MAX_HISTORY_SIZE: Final = "max_history_size"
|
CONF_MAX_HISTORY_SIZE: Final = "max_history_size" # Correct constant name
|
||||||
CONF_IS_ANTHROPIC: Final = "is_anthropic"
|
CONF_IS_ANTHROPIC: Final = "is_anthropic"
|
||||||
CONF_CONTEXT_MESSAGES: Final = "context_messages"
|
CONF_CONTEXT_MESSAGES: Final = "context_messages"
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ DEFAULT_REQUEST_INTERVAL: Final = 1.0
|
|||||||
DEFAULT_TIMEOUT: Final = 30
|
DEFAULT_TIMEOUT: Final = 30
|
||||||
DEFAULT_MAX_HISTORY: Final = 50
|
DEFAULT_MAX_HISTORY: Final = 50
|
||||||
DEFAULT_NAME: Final = "HA Text AI"
|
DEFAULT_NAME: Final = "HA Text AI"
|
||||||
|
DEFAULT_NAME_PREFIX = "ha_text_ai"
|
||||||
DEFAULT_CONTEXT_MESSAGES: Final = 5
|
DEFAULT_CONTEXT_MESSAGES: Final = 5
|
||||||
|
|
||||||
# Parameter constraints
|
# Parameter constraints
|
||||||
@@ -164,7 +165,7 @@ SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
|
|||||||
vol.Required(CONF_INSTANCE): cv.string,
|
vol.Required(CONF_INSTANCE): cv.string,
|
||||||
vol.Optional("limit", default=10): vol.All(
|
vol.Optional("limit", default=10): vol.All(
|
||||||
vol.Coerce(int),
|
vol.Coerce(int),
|
||||||
vol.Range(min=1, max=100)
|
vol.Range(min=1, max=100),
|
||||||
),
|
),
|
||||||
vol.Optional("filter_model"): cv.string
|
vol.Optional("filter_model"): cv.string
|
||||||
})
|
})
|
||||||
@@ -189,7 +190,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
vol.Coerce(float),
|
vol.Coerce(float),
|
||||||
vol.Range(min=MIN_REQUEST_INTERVAL, max=MAX_REQUEST_INTERVAL)
|
vol.Range(min=MIN_REQUEST_INTERVAL, max=MAX_REQUEST_INTERVAL)
|
||||||
),
|
),
|
||||||
vol.Optional(CONF_MAX_HISTORY_SIZE, default=DEFAULT_MAX_HISTORY): vol.All(
|
vol.Optional(CONF_MAX_HISTORY_SIZE, default=DEFAULT_MAX_HISTORY): vol.All( # Correct usage
|
||||||
vol.Coerce(int),
|
vol.Coerce(int),
|
||||||
vol.Range(min=1, max=100),
|
vol.Range(min=1, max=100),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import traceback
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
@@ -9,6 +10,8 @@ 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
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.const import CONF_NAME
|
||||||
|
from .config_flow import normalize_name
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@@ -21,11 +24,16 @@ from .const import (
|
|||||||
DEFAULT_TEMPERATURE,
|
DEFAULT_TEMPERATURE,
|
||||||
DEFAULT_MAX_HISTORY,
|
DEFAULT_MAX_HISTORY,
|
||||||
DEFAULT_CONTEXT_MESSAGES,
|
DEFAULT_CONTEXT_MESSAGES,
|
||||||
|
DEFAULT_NAME_PREFIX,
|
||||||
|
CONF_MAX_HISTORY_SIZE,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class HATextAICoordinator(DataUpdateCoordinator):
|
class HATextAICoordinator(DataUpdateCoordinator):
|
||||||
|
"""The HA Text AI coordinator."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -41,6 +49,12 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize coordinator."""
|
"""Initialize coordinator."""
|
||||||
self.instance_name = instance_name
|
self.instance_name = instance_name
|
||||||
|
self.normalized_name = None
|
||||||
|
|
||||||
|
# Use the normalize_name function from config_flow to ensure consistency
|
||||||
|
from .config_flow import normalize_name
|
||||||
|
self.normalized_name = normalize_name(instance_name)
|
||||||
|
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.client = client
|
self.client = client
|
||||||
self.model = model
|
self.model = model
|
||||||
@@ -61,7 +75,7 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
"total_errors": 0,
|
"total_errors": 0,
|
||||||
"average_latency": 0,
|
"average_latency": 0,
|
||||||
"max_latency": 0,
|
"max_latency": 0,
|
||||||
"min_latency": float('inf'),
|
"min_latency": float("inf"),
|
||||||
},
|
},
|
||||||
"last_response": {
|
"last_response": {
|
||||||
"timestamp": dt_util.utcnow().isoformat(),
|
"timestamp": dt_util.utcnow().isoformat(),
|
||||||
@@ -69,7 +83,8 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
"response": "",
|
"response": "",
|
||||||
"model": model,
|
"model": model,
|
||||||
"instance": instance_name,
|
"instance": instance_name,
|
||||||
"error": None
|
"normalized_name": self.normalized_name,
|
||||||
|
"error": None,
|
||||||
},
|
},
|
||||||
"is_processing": False,
|
"is_processing": False,
|
||||||
"is_rate_limited": False,
|
"is_rate_limited": False,
|
||||||
@@ -105,13 +120,17 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
self.last_response = self._initial_state["last_response"].copy()
|
self.last_response = self._initial_state["last_response"].copy()
|
||||||
self._start_time = dt_util.utcnow()
|
self._start_time = dt_util.utcnow()
|
||||||
|
|
||||||
_LOGGER.info(f"Initialized HA Text AI coordinator with instance: {instance_name}")
|
_LOGGER.info(
|
||||||
|
f"Initialized HA Text AI coordinator with instance: {instance_name}"
|
||||||
|
)
|
||||||
|
|
||||||
async def _async_update_data(self) -> Dict[str, Any]:
|
async def _async_update_data(self) -> Dict[str, Any]:
|
||||||
"""Update data via library."""
|
"""Update data via library."""
|
||||||
try:
|
try:
|
||||||
current_state = self._get_current_state()
|
current_state = self._get_current_state()
|
||||||
_LOGGER.debug(f"Updating data for {self.instance_name}, current state: {current_state}")
|
_LOGGER.debug(
|
||||||
|
f"Updating data for {self.instance_name}, current state: {current_state}"
|
||||||
|
)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"state": current_state,
|
"state": current_state,
|
||||||
@@ -125,6 +144,7 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
"system_prompt": self._system_prompt,
|
"system_prompt": self._system_prompt,
|
||||||
"history_size": len(self._conversation_history),
|
"history_size": len(self._conversation_history),
|
||||||
"conversation_history": self._conversation_history,
|
"conversation_history": self._conversation_history,
|
||||||
|
"normalized_name": self.normalized_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Validate data
|
# Validate data
|
||||||
@@ -141,12 +161,15 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
async def async_update_ha_state(self) -> None:
|
async def async_update_ha_state(self) -> None:
|
||||||
"""Update Home Assistant state."""
|
"""Update Home Assistant state."""
|
||||||
try:
|
try:
|
||||||
_LOGGER.debug(f"Requesting state update for {self.instance_name}")
|
_LOGGER.debug(
|
||||||
|
f"Requesting state update for {self.instance_name} (normalized: {self.normalized_name})"
|
||||||
|
)
|
||||||
await self.async_request_refresh()
|
await self.async_request_refresh()
|
||||||
|
|
||||||
# Force update of all entities
|
# Force update of all entities
|
||||||
|
entity_id_base = f"sensor.ha_text_ai_{self.normalized_name.lower()}"
|
||||||
for entity_id in self.hass.states.async_entity_ids():
|
for entity_id in self.hass.states.async_entity_ids():
|
||||||
if entity_id.startswith(f"sensor.ha_text_ai_{self.instance_name}"):
|
if entity_id.startswith(entity_id_base):
|
||||||
self.hass.states.async_set(entity_id, self._get_current_state())
|
self.hass.states.async_set(entity_id, self._get_current_state())
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@@ -164,15 +187,67 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
return STATE_ERROR
|
return STATE_ERROR
|
||||||
return STATE_READY
|
return STATE_READY
|
||||||
|
|
||||||
def _calculate_context_tokens(self, messages: List[Dict[str, str]]) -> int:
|
def _calculate_context_tokens(self, messages: List[Dict[str, str]], model: str = None) -> int:
|
||||||
|
"""
|
||||||
|
Estimate tokens for conversation context.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages: List of message dictionaries
|
||||||
|
model: Optional model name for provider-specific estimation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Estimated number of tokens
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
|
# Anthropic specific token counting
|
||||||
if self.is_anthropic and hasattr(self.client, 'count_tokens'):
|
if self.is_anthropic and hasattr(self.client, 'count_tokens'):
|
||||||
return sum(self.client.count_tokens(msg['content']) for msg in messages)
|
return sum(self.client.count_tokens(msg['content']) for msg in messages)
|
||||||
|
|
||||||
return sum(len(msg['content']) // 4 for msg in messages)
|
def estimate_tokens(text: str) -> int:
|
||||||
|
"""
|
||||||
|
Flexible token estimation algorithm.
|
||||||
|
|
||||||
|
Heuristics:
|
||||||
|
- Count words
|
||||||
|
- Estimate special characters
|
||||||
|
- Fallback to character-based estimation
|
||||||
|
"""
|
||||||
|
# Word-based estimation
|
||||||
|
words = len(text.split())
|
||||||
|
|
||||||
|
# Special character handling
|
||||||
|
special_chars = sum(1 for char in text if not char.isalnum())
|
||||||
|
|
||||||
|
# Character-based fallback
|
||||||
|
char_tokens = len(text) // 4
|
||||||
|
|
||||||
|
# Combine estimations with bias towards words
|
||||||
|
total_tokens = (words * 1.5) + (special_chars * 0.5) + char_tokens
|
||||||
|
|
||||||
|
return max(int(total_tokens), words)
|
||||||
|
|
||||||
|
# Calculate total tokens across all messages
|
||||||
|
total_tokens = sum(estimate_tokens(msg['content']) for msg in messages)
|
||||||
|
|
||||||
|
# Logging for debugging
|
||||||
|
_LOGGER.debug(
|
||||||
|
f"Token Estimation: "
|
||||||
|
f"Messages: {len(messages)}, "
|
||||||
|
f"Estimated Tokens: {total_tokens}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return total_tokens
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_LOGGER.warning(f"Error calculating context tokens: {e}")
|
# Safe fallback with detailed logging
|
||||||
return 0
|
_LOGGER.warning(
|
||||||
|
f"Token estimation failed. "
|
||||||
|
f"Error: {e}. "
|
||||||
|
f"Using conservative estimation."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Conservative token estimation
|
||||||
|
return len(messages) * 100
|
||||||
|
|
||||||
async def async_ask_question(
|
async def async_ask_question(
|
||||||
self,
|
self,
|
||||||
@@ -183,7 +258,24 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
system_prompt: Optional[str] = None,
|
system_prompt: Optional[str] = None,
|
||||||
context_messages: Optional[int] = None,
|
context_messages: Optional[int] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Process a question with optional parameters."""
|
"""
|
||||||
|
Process a question with optional parameters.
|
||||||
|
|
||||||
|
This method is a direct wrapper around async_process_question,
|
||||||
|
allowing flexible AI interaction with optional model, temperature,
|
||||||
|
and context customization.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
question: The input question or prompt
|
||||||
|
model: Optional AI model to use
|
||||||
|
temperature: Optional response creativity level
|
||||||
|
max_tokens: Optional maximum response length
|
||||||
|
system_prompt: Optional system-level instruction
|
||||||
|
context_messages: Optional number of context messages to include
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Full response dictionary from the AI
|
||||||
|
"""
|
||||||
return await self.async_process_question(
|
return await self.async_process_question(
|
||||||
question, model, temperature, max_tokens, system_prompt, context_messages
|
question, model, temperature, max_tokens, system_prompt, context_messages
|
||||||
)
|
)
|
||||||
@@ -197,47 +289,83 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
system_prompt: Optional[str] = None,
|
system_prompt: Optional[str] = None,
|
||||||
context_messages: Optional[int] = None,
|
context_messages: Optional[int] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
temp_context_messages = context_messages or self.context_messages
|
"""
|
||||||
|
Enhanced question processing with intelligent token management.
|
||||||
if not question:
|
"""
|
||||||
raise ValueError("Question cannot be empty")
|
|
||||||
|
|
||||||
_LOGGER.debug(f"Processing question for instance {self.instance_name}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._is_processing = True
|
self._is_processing = True
|
||||||
await self.async_update_ha_state()
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
|
temp_context_messages = context_messages or self.context_messages
|
||||||
temp_model = model or self.model
|
temp_model = model or self.model
|
||||||
temp_temperature = temperature or self.temperature
|
temp_temperature = temperature or self.temperature
|
||||||
temp_max_tokens = max_tokens or self.max_tokens
|
temp_max_tokens = max_tokens or self.max_tokens
|
||||||
temp_system_prompt = system_prompt or self._system_prompt
|
temp_system_prompt = system_prompt or self._system_prompt
|
||||||
|
|
||||||
|
# Start timing
|
||||||
start_time = dt_util.utcnow()
|
start_time = dt_util.utcnow()
|
||||||
|
|
||||||
|
# Prepare messages with system prompt
|
||||||
messages = []
|
messages = []
|
||||||
if temp_system_prompt:
|
if temp_system_prompt:
|
||||||
if self.is_anthropic:
|
|
||||||
system_content = f"\n\nHuman: {temp_system_prompt}\n\nAssistant: I understand and will follow these instructions."
|
|
||||||
messages.append({"role": "user", "content": system_content})
|
|
||||||
else:
|
|
||||||
messages.append({"role": "system", "content": temp_system_prompt})
|
messages.append({"role": "system", "content": temp_system_prompt})
|
||||||
|
|
||||||
# Add conversation history
|
# Context history management
|
||||||
context_history = self._conversation_history[-temp_context_messages:]
|
context_history = self._conversation_history[-temp_context_messages:]
|
||||||
|
|
||||||
|
# Comprehensive token calculation
|
||||||
|
context_tokens = self._calculate_context_tokens(
|
||||||
|
[{"content": entry["question"]} for entry in context_history] +
|
||||||
|
[{"content": entry["response"]} for entry in context_history] +
|
||||||
|
[{"content": question}],
|
||||||
|
temp_model
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dynamic token allocation
|
||||||
|
available_tokens = max(0, temp_max_tokens - context_tokens)
|
||||||
|
|
||||||
|
# Context trimming if over token limit
|
||||||
|
if context_tokens > temp_max_tokens:
|
||||||
|
_LOGGER.warning(
|
||||||
|
f"Token limit exceeded. "
|
||||||
|
f"Context: {context_tokens}, "
|
||||||
|
f"Max: {temp_max_tokens}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Intelligent context reduction
|
||||||
|
while context_tokens > temp_max_tokens // 2 and context_history:
|
||||||
|
context_history.pop(0)
|
||||||
|
context_tokens = self._calculate_context_tokens(
|
||||||
|
[{"content": entry["question"]} for entry in context_history] +
|
||||||
|
[{"content": entry["response"]} for entry in context_history] +
|
||||||
|
[{"content": question}],
|
||||||
|
temp_model
|
||||||
|
)
|
||||||
|
|
||||||
|
# Rebuild messages with trimmed context
|
||||||
for entry in context_history:
|
for entry in context_history:
|
||||||
messages.append({"role": "user", "content": entry["question"]})
|
messages.append({"role": "user", "content": entry["question"]})
|
||||||
messages.append({"role": "assistant", "content": entry["response"]})
|
messages.append({"role": "assistant", "content": entry["response"]})
|
||||||
|
|
||||||
messages.append({"role": "user", "content": question})
|
messages.append({"role": "user", "content": question})
|
||||||
|
|
||||||
|
# Detailed token logging
|
||||||
|
_LOGGER.debug(
|
||||||
|
f"Token Analysis: "
|
||||||
|
f"Context Tokens: {context_tokens}, "
|
||||||
|
f"Max Tokens: {temp_max_tokens}, "
|
||||||
|
f"Available Tokens: {available_tokens}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prepare API call with dynamic token management
|
||||||
kwargs = {
|
kwargs = {
|
||||||
"model": temp_model,
|
"model": temp_model,
|
||||||
"temperature": temp_temperature,
|
"temperature": temp_temperature,
|
||||||
"max_tokens": temp_max_tokens,
|
"max_tokens": min(temp_max_tokens, available_tokens),
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Process message
|
||||||
response = await self.async_process_message(question, **kwargs)
|
response = await self.async_process_message(question, **kwargs)
|
||||||
|
|
||||||
# Update metrics
|
# Update metrics
|
||||||
@@ -272,7 +400,7 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
"response": response["content"],
|
"response": response["content"],
|
||||||
"model": kwargs.get("model", self.model),
|
"model": kwargs.get("model", self.model),
|
||||||
"instance": self.instance_name,
|
"instance": self.instance_name,
|
||||||
"error": None
|
"error": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@@ -283,20 +411,26 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
|
|
||||||
async def _process_anthropic_message(self, question: str, **kwargs) -> dict:
|
async def _process_anthropic_message(self, question: str, **kwargs) -> dict:
|
||||||
"""Process message using Anthropic API."""
|
"""Process message using Anthropic API."""
|
||||||
|
try:
|
||||||
|
_LOGGER.debug(f"Anthropic API call: model={kwargs['model']}, max_tokens={kwargs['max_tokens']}")
|
||||||
response = await self.client.messages.create(
|
response = await self.client.messages.create(
|
||||||
model=kwargs["model"],
|
model=kwargs["model"],
|
||||||
max_tokens=kwargs["max_tokens"],
|
max_tokens=kwargs["max_tokens"],
|
||||||
messages=kwargs["messages"],
|
messages=kwargs["messages"],
|
||||||
temperature=kwargs["temperature"],
|
temperature=kwargs["temperature"],
|
||||||
)
|
)
|
||||||
|
_LOGGER.debug(f"Anthropic response: tokens={response.usage}")
|
||||||
return {
|
return {
|
||||||
"content": response.content[0].text,
|
"content": response.content[0].text,
|
||||||
"tokens": {
|
"tokens": {
|
||||||
"prompt": response.usage.input_tokens,
|
"prompt": response.usage.input_tokens,
|
||||||
"completion": response.usage.output_tokens,
|
"completion": response.usage.output_tokens,
|
||||||
"total": response.usage.input_tokens + response.usage.output_tokens
|
"total": response.usage.input_tokens + response.usage.output_tokens,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(f"Anthropic API error: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
async def _process_openai_message(self, question: str, **kwargs) -> dict:
|
async def _process_openai_message(self, question: str, **kwargs) -> dict:
|
||||||
"""Process message using OpenAI API."""
|
"""Process message using OpenAI API."""
|
||||||
@@ -313,8 +447,8 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
"tokens": {
|
"tokens": {
|
||||||
"prompt": response["usage"]["prompt_tokens"],
|
"prompt": response["usage"]["prompt_tokens"],
|
||||||
"completion": response["usage"]["completion_tokens"],
|
"completion": response["usage"]["completion_tokens"],
|
||||||
"total": response["usage"]["total_tokens"]
|
"total": response["usage"]["total_tokens"],
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_LOGGER.error(f"Error in OpenAI API call: {str(e)}")
|
_LOGGER.error(f"Error in OpenAI API call: {str(e)}")
|
||||||
@@ -339,29 +473,68 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
|
|
||||||
def _update_history(self, question: str, response: dict) -> None:
|
def _update_history(self, question: str, response: dict) -> None:
|
||||||
"""Update conversation history."""
|
"""Update conversation history."""
|
||||||
self._conversation_history.append({
|
self._conversation_history.append(
|
||||||
|
{
|
||||||
"timestamp": dt_util.utcnow().isoformat(),
|
"timestamp": dt_util.utcnow().isoformat(),
|
||||||
"question": question,
|
"question": question,
|
||||||
"response": response["content"]
|
"response": response["content"],
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
while len(self._conversation_history) > self.max_history_size:
|
while len(self._conversation_history) > self.max_history_size:
|
||||||
self._conversation_history.pop(0)
|
self._conversation_history.pop(0)
|
||||||
|
|
||||||
def _handle_error(self, error: Exception) -> None:
|
def _handle_error(self, error: Exception) -> None:
|
||||||
"""Handle error and update metrics."""
|
"""
|
||||||
|
Enhanced error handling with comprehensive diagnostics.
|
||||||
|
|
||||||
|
Captures detailed error information, tracks error metrics,
|
||||||
|
and provides context for troubleshooting AI processing issues.
|
||||||
|
"""
|
||||||
self._performance_metrics["total_errors"] += 1
|
self._performance_metrics["total_errors"] += 1
|
||||||
self._performance_metrics["failed_requests"] += 1
|
self._performance_metrics["failed_requests"] += 1
|
||||||
|
|
||||||
self.last_response = {
|
error_details = {
|
||||||
"timestamp": dt_util.utcnow().isoformat(),
|
"timestamp": dt_util.utcnow().isoformat(),
|
||||||
"question": "",
|
|
||||||
"response": "",
|
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"instance": self.instance_name,
|
"instance": self.instance_name,
|
||||||
"error": str(error)
|
"error_message": str(error),
|
||||||
|
"error_type": type(error).__name__,
|
||||||
|
"traceback": traceback.format_exc() if _LOGGER.isEnabledFor(logging.DEBUG) else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Specific error type handling
|
||||||
|
error_mapping = {
|
||||||
|
HomeAssistantError: {"is_ha_error": True},
|
||||||
|
ConnectionError: {
|
||||||
|
"is_connection_error": True,
|
||||||
|
"is_rate_limited": True
|
||||||
|
},
|
||||||
|
TimeoutError: {"is_timeout": True},
|
||||||
|
PermissionError: {"is_permission_denied": True},
|
||||||
|
ValueError: {"is_validation_error": True}
|
||||||
|
}
|
||||||
|
|
||||||
|
for error_type, error_flags in error_mapping.items():
|
||||||
|
if isinstance(error, error_type):
|
||||||
|
error_details.update(error_flags)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Update system state based on error type
|
||||||
|
if error_details.get("is_rate_limited"):
|
||||||
|
self._is_rate_limited = True
|
||||||
|
_LOGGER.warning(f"Rate limit detected for {self.instance_name}")
|
||||||
|
|
||||||
|
if error_details.get("is_connection_error"):
|
||||||
|
self.endpoint_status = "unavailable"
|
||||||
|
|
||||||
|
self.last_response = error_details
|
||||||
|
_LOGGER.error(f"AI Processing Error: {error_details}")
|
||||||
|
|
||||||
|
# Optional: Add more sophisticated error tracking or notification logic
|
||||||
|
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||||
|
_LOGGER.debug(f"Full Error Traceback: {error_details['traceback']}")
|
||||||
|
|
||||||
async def async_clear_history(self) -> None:
|
async def async_clear_history(self) -> None:
|
||||||
"""Clear conversation history."""
|
"""Clear conversation history."""
|
||||||
self._conversation_history = []
|
self._conversation_history = []
|
||||||
@@ -375,3 +548,8 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
"""Set system prompt."""
|
"""Set system prompt."""
|
||||||
self._system_prompt = prompt
|
self._system_prompt = prompt
|
||||||
await self.async_update_ha_state()
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
|
async def async_shutdown(self) -> None:
|
||||||
|
"""Shutdown coordinator."""
|
||||||
|
_LOGGER.debug(f"Shutting down coordinator for {self.instance_name}")
|
||||||
|
self.hass.data[DOMAIN].pop(self.instance_name, None)
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 678 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
@@ -23,6 +23,6 @@
|
|||||||
"single_config_entry": false,
|
"single_config_entry": false,
|
||||||
"ssdp": [],
|
"ssdp": [],
|
||||||
"usb": [],
|
"usb": [],
|
||||||
"version": "2.0.0",
|
"version": "2.0.3-beta",
|
||||||
"zeroconf": []
|
"zeroconf": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,25 +58,39 @@ from .const import (
|
|||||||
ENTITY_ICON,
|
ENTITY_ICON,
|
||||||
ENTITY_ICON_ERROR,
|
ENTITY_ICON_ERROR,
|
||||||
ENTITY_ICON_PROCESSING,
|
ENTITY_ICON_PROCESSING,
|
||||||
|
DEFAULT_NAME_PREFIX,
|
||||||
|
CONF_MAX_HISTORY_SIZE,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .coordinator import HATextAICoordinator
|
from .coordinator import HATextAICoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the HA Text AI sensor."""
|
"""Set up the HA Text AI sensor."""
|
||||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
_LOGGER.debug(f"Starting sensor setup for entry: {entry.entry_id}")
|
||||||
instance_name = coordinator.instance_name
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
_LOGGER.debug(f"Found coordinator for entry {entry.entry_id}")
|
||||||
|
|
||||||
|
instance_name = coordinator.instance_name
|
||||||
_LOGGER.debug(f"Setting up sensor with instance: {instance_name}")
|
_LOGGER.debug(f"Setting up sensor with instance: {instance_name}")
|
||||||
|
|
||||||
sensor = HATextAISensor(coordinator, entry)
|
sensor = HATextAISensor(coordinator, entry)
|
||||||
|
_LOGGER.debug(f"Created sensor instance: {sensor.entity_id}")
|
||||||
|
|
||||||
async_add_entities([sensor], True)
|
async_add_entities([sensor], True)
|
||||||
|
_LOGGER.debug(f"Added sensor entity: {sensor.entity_id}")
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.exception(f"Error setting up sensor: {err}")
|
||||||
|
raise
|
||||||
|
|
||||||
class HATextAISensor(CoordinatorEntity, SensorEntity):
|
class HATextAISensor(CoordinatorEntity, SensorEntity):
|
||||||
"""HA Text AI Sensor."""
|
"""HA Text AI Sensor."""
|
||||||
@@ -89,19 +103,30 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
|
_LOGGER.debug(f"Initializing sensor with config entry: {config_entry.data}")
|
||||||
|
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
self._config_entry = config_entry
|
self._config_entry = config_entry
|
||||||
self._instance_name = coordinator.instance_name
|
self._instance_name = coordinator.instance_name
|
||||||
|
self._normalized_name = coordinator.normalized_name
|
||||||
|
|
||||||
|
_LOGGER.debug(f"Instance name: {self._instance_name}")
|
||||||
|
_LOGGER.debug(f"Normalized name: {self._normalized_name}")
|
||||||
|
|
||||||
self._conversation_history = []
|
self._conversation_history = []
|
||||||
self._system_prompt = None
|
self._system_prompt = None
|
||||||
|
|
||||||
self._attr_name = f"HA Text AI {self._instance_name}"
|
self._attr_name = f"HA Text AI {self._instance_name}"
|
||||||
self.entity_id = f"sensor.ha_text_ai_{slugify(self._instance_name)}"
|
self.entity_id = f"sensor.ha_text_ai_{self._normalized_name}"
|
||||||
self._attr_unique_id = f"{config_entry.entry_id}"
|
self._attr_unique_id = f"{config_entry.entry_id}"
|
||||||
|
|
||||||
|
_LOGGER.debug(f"Created sensor with entity_id: {self.entity_id}")
|
||||||
|
_LOGGER.debug(f"Sensor name: {self._attr_name}")
|
||||||
|
_LOGGER.debug(f"Unique ID: {self._attr_unique_id}")
|
||||||
|
|
||||||
self.entity_description = SensorEntityDescription(
|
self.entity_description = SensorEntityDescription(
|
||||||
key=f"ha_text_ai_{self._instance_name}",
|
key=f"ha_text_ai_{self._normalized_name.lower()}",
|
||||||
entity_registry_enabled_default=True,
|
entity_registry_enabled_default=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -118,13 +143,15 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||||
name=self._attr_name, # Используем имя сенсора
|
name=self._attr_name,
|
||||||
manufacturer="Community",
|
manufacturer="Community",
|
||||||
model=f"{model} ({api_provider} provider)",
|
model=f"{model} ({api_provider} provider)",
|
||||||
sw_version="1.0.0",
|
sw_version="1.0.0",
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER.debug(f"Initialized sensor: {self.entity_id} for instance: {self._instance_name}")
|
_LOGGER.debug(
|
||||||
|
f"Initialized sensor: {self.entity_id} for instance: {self._instance_name}"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
@@ -180,11 +207,14 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
data = self.coordinator.data
|
data = self.coordinator.data
|
||||||
attributes = {
|
attributes = {
|
||||||
ATTR_MODEL: self._config_entry.data.get(CONF_MODEL, "Unknown"),
|
ATTR_MODEL: self._config_entry.data.get(CONF_MODEL, "Unknown"),
|
||||||
ATTR_API_PROVIDER: self._config_entry.data.get(CONF_API_PROVIDER, "Unknown"),
|
ATTR_API_PROVIDER: self._config_entry.data.get(
|
||||||
|
CONF_API_PROVIDER, "Unknown"
|
||||||
|
),
|
||||||
ATTR_API_STATUS: self._current_state,
|
ATTR_API_STATUS: self._current_state,
|
||||||
ATTR_TOTAL_ERRORS: self._error_count,
|
ATTR_TOTAL_ERRORS: self._error_count,
|
||||||
ATTR_LAST_ERROR: self._last_error,
|
ATTR_LAST_ERROR: self._last_error,
|
||||||
"instance_name": self._instance_name,
|
"instance_name": self._instance_name,
|
||||||
|
"normalized_name": self._normalized_name,
|
||||||
ATTR_SYSTEM_PROMPT: data.get("system_prompt"),
|
ATTR_SYSTEM_PROMPT: data.get("system_prompt"),
|
||||||
ATTR_IS_PROCESSING: data.get("is_processing", False),
|
ATTR_IS_PROCESSING: data.get("is_processing", False),
|
||||||
ATTR_IS_RATE_LIMITED: data.get("is_rate_limited", False),
|
ATTR_IS_RATE_LIMITED: data.get("is_rate_limited", False),
|
||||||
@@ -199,32 +229,40 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
metrics = data.get("metrics", {})
|
metrics = data.get("metrics", {})
|
||||||
if isinstance(metrics, dict):
|
if isinstance(metrics, dict):
|
||||||
self._metrics = metrics
|
self._metrics = metrics
|
||||||
attributes.update({
|
attributes.update(
|
||||||
|
{
|
||||||
METRIC_TOTAL_TOKENS: metrics.get("total_tokens", 0),
|
METRIC_TOTAL_TOKENS: metrics.get("total_tokens", 0),
|
||||||
METRIC_PROMPT_TOKENS: metrics.get("prompt_tokens", 0),
|
METRIC_PROMPT_TOKENS: metrics.get("prompt_tokens", 0),
|
||||||
METRIC_COMPLETION_TOKENS: metrics.get("completion_tokens", 0),
|
METRIC_COMPLETION_TOKENS: metrics.get("completion_tokens", 0),
|
||||||
METRIC_SUCCESSFUL_REQUESTS: metrics.get("successful_requests", 0),
|
METRIC_SUCCESSFUL_REQUESTS: metrics.get(
|
||||||
|
"successful_requests", 0
|
||||||
|
),
|
||||||
METRIC_FAILED_REQUESTS: metrics.get("failed_requests", 0),
|
METRIC_FAILED_REQUESTS: metrics.get("failed_requests", 0),
|
||||||
METRIC_AVERAGE_LATENCY: metrics.get("average_latency", 0),
|
METRIC_AVERAGE_LATENCY: metrics.get("average_latency", 0),
|
||||||
METRIC_MAX_LATENCY: metrics.get("max_latency", 0),
|
METRIC_MAX_LATENCY: metrics.get("max_latency", 0),
|
||||||
METRIC_MIN_LATENCY: metrics.get("min_latency", float("inf")),
|
METRIC_MIN_LATENCY: metrics.get("min_latency", float("inf")),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Add last response
|
# Add last response
|
||||||
last_response = data.get("last_response", {})
|
last_response = data.get("last_response", {})
|
||||||
if isinstance(last_response, dict):
|
if isinstance(last_response, dict):
|
||||||
self._last_response = last_response
|
self._last_response = last_response
|
||||||
attributes.update({
|
attributes.update(
|
||||||
|
{
|
||||||
ATTR_RESPONSE: last_response.get("response", ""),
|
ATTR_RESPONSE: last_response.get("response", ""),
|
||||||
ATTR_QUESTION: last_response.get("question", ""),
|
ATTR_QUESTION: last_response.get("question", ""),
|
||||||
"last_model": last_response.get("model", ""),
|
"last_model": last_response.get("model", ""),
|
||||||
"last_timestamp": last_response.get("timestamp", ""),
|
"last_timestamp": last_response.get("timestamp", ""),
|
||||||
"last_error": last_response.get("error"),
|
"last_error": last_response.get("error"),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Add performance metrics if available
|
# Add performance metrics if available
|
||||||
if ATTR_PERFORMANCE_METRICS in data:
|
if ATTR_PERFORMANCE_METRICS in data:
|
||||||
attributes[ATTR_PERFORMANCE_METRICS] = data[ATTR_PERFORMANCE_METRICS]
|
attributes[ATTR_PERFORMANCE_METRICS] = data[
|
||||||
|
ATTR_PERFORMANCE_METRICS
|
||||||
|
]
|
||||||
|
|
||||||
# Add API version if available
|
# Add API version if available
|
||||||
if ATTR_API_VERSION in data:
|
if ATTR_API_VERSION in data:
|
||||||
@@ -288,7 +326,7 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
"Error handling update for %s: %s",
|
"Error handling update for %s: %s",
|
||||||
self.entity_id,
|
self.entity_id,
|
||||||
err,
|
err,
|
||||||
exc_info=True
|
exc_info=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|||||||
@@ -40,15 +40,14 @@ ask_question:
|
|||||||
min: 1
|
min: 1
|
||||||
max: 20
|
max: 20
|
||||||
step: 1
|
step: 1
|
||||||
mode: box
|
mode: slider
|
||||||
|
|
||||||
model:
|
model:
|
||||||
name: Model
|
name: Model
|
||||||
description: "Select AI model to use (optional, overrides default setting)"
|
description: "Select AI model to use (optional, overrides default setting)"
|
||||||
required: false
|
required: false
|
||||||
selector:
|
selector:
|
||||||
text:
|
text: {}
|
||||||
multiline: false
|
|
||||||
|
|
||||||
temperature:
|
temperature:
|
||||||
name: Temperature
|
name: Temperature
|
||||||
@@ -73,3 +72,98 @@ ask_question:
|
|||||||
max: 4096
|
max: 4096
|
||||||
step: 1
|
step: 1
|
||||||
mode: box
|
mode: box
|
||||||
|
|
||||||
|
clear_history:
|
||||||
|
name: Clear History
|
||||||
|
description: >-
|
||||||
|
Delete all stored questions and responses from the conversation history
|
||||||
|
fields:
|
||||||
|
instance:
|
||||||
|
name: Instance
|
||||||
|
description: Name of the HA Text AI instance to clear history for
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
integration: ha_text_ai
|
||||||
|
domain: sensor
|
||||||
|
|
||||||
|
get_history:
|
||||||
|
name: Get History
|
||||||
|
description: Retrieve conversation history with optional filtering and sorting
|
||||||
|
fields:
|
||||||
|
instance:
|
||||||
|
name: Instance
|
||||||
|
description: Name of the HA Text AI instance to get history from
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
integration: ha_text_ai
|
||||||
|
domain: sensor
|
||||||
|
|
||||||
|
limit:
|
||||||
|
name: Limit
|
||||||
|
description: Number of conversations to return (1-100)
|
||||||
|
required: false
|
||||||
|
default: 10
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1
|
||||||
|
max: 100
|
||||||
|
step: 1
|
||||||
|
mode: box
|
||||||
|
|
||||||
|
filter_model:
|
||||||
|
name: Filter Model
|
||||||
|
description: Filter conversations by specific AI model
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
multiline: false
|
||||||
|
|
||||||
|
start_date:
|
||||||
|
name: Start Date
|
||||||
|
description: Filter conversations starting from this date/time
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
multiline: false
|
||||||
|
|
||||||
|
include_metadata:
|
||||||
|
name: Include Metadata
|
||||||
|
description: Include additional information like tokens used, response time, etc.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
selector:
|
||||||
|
boolean:
|
||||||
|
|
||||||
|
sort_order:
|
||||||
|
name: Sort Order
|
||||||
|
description: Sort order for results (newest or oldest first)
|
||||||
|
required: false
|
||||||
|
default: newest
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- newest
|
||||||
|
- oldest
|
||||||
|
|
||||||
|
set_system_prompt:
|
||||||
|
name: Set System Prompt
|
||||||
|
description: Set default system behavior instructions for all future conversations
|
||||||
|
fields:
|
||||||
|
instance:
|
||||||
|
name: Instance
|
||||||
|
description: Name of the HA Text AI instance to set system prompt for
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
integration: ha_text_ai
|
||||||
|
domain: sensor
|
||||||
|
|
||||||
|
prompt:
|
||||||
|
name: System Prompt
|
||||||
|
description: Instructions that define how the AI should behave and respond
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
multiline: true
|
||||||
|
|||||||
@@ -3,24 +3,26 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"provider": {
|
"provider": {
|
||||||
"title": "KI-Anbieter auswählen",
|
"title": "KI-Anbieter auswählen",
|
||||||
"description": "Wählen Sie den KI-Dienst-Anbieter für diese Instanz",
|
"description": "Wählen Sie den KI-Dienstanbieter für diese Instanz aus.",
|
||||||
"data": {
|
"data": {
|
||||||
"api_provider": "API-Anbieter",
|
"api_provider": "API-Anbieter",
|
||||||
"context_messages": "Anzahl der zu speichernden Kontextnachrichten (1-20)"
|
"context_messages": "Anzahl der zu behaltenden Kontextnachrichten (1-20)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"title": "HA Text AI Instanz konfigurieren",
|
"title": "HA Text AI-Instanz konfigurieren",
|
||||||
"description": "Richten Sie eine neue KI-Assistenten-Instanz mit Ihrem ausgewählten Anbieter ein",
|
"description": "Richten Sie eine neue KI-Assistenteninstanz mit Ihrem ausgewählten Anbieter ein.",
|
||||||
"data": {
|
"data": {
|
||||||
"name": "Instanzname (z.B. 'GPT Assistent', 'Claude Helfer')",
|
"name": "Instanzname (z. B. 'GPT-Assistent', 'Claude-Helfer')",
|
||||||
"api_key": "API-Schlüssel für Authentifizierung",
|
"api_key": "API-Schlüssel zur Authentifizierung",
|
||||||
"model": "Zu verwendendes KI-Modell",
|
"model": "Zu verwendendes KI-Modell",
|
||||||
"temperature": "Antwort-Kreativität (0-2, niedriger = fokussierter)",
|
"temperature": "Antwortkreativität (0-2, niedriger = fokussierter)",
|
||||||
"max_tokens": "Maximale Antwortlänge (1-4096 Token)",
|
"max_tokens": "Maximale Antwortlänge (1-4096 Token)",
|
||||||
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
|
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
|
||||||
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)",
|
"api_provider": "API-Anbieter",
|
||||||
"context_messages": "Anzahl der zu speichernden Kontextnachrichten (1-20)"
|
"request_interval": "Mindestzeit zwischen Anfragen (0,1-60 Sekunden)",
|
||||||
|
"context_messages": "Anzahl der zu behaltenden Kontextnachrichten (1-20)",
|
||||||
|
"max_history_size": "Maximale Größe des Konversationsverlaufs (1-100)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -28,32 +30,35 @@
|
|||||||
"name_exists": "Eine Instanz mit diesem Namen existiert bereits",
|
"name_exists": "Eine Instanz mit diesem Namen existiert bereits",
|
||||||
"invalid_name": "Ungültiger Instanzname",
|
"invalid_name": "Ungültiger Instanzname",
|
||||||
"invalid_auth": "Authentifizierung fehlgeschlagen - überprüfen Sie Ihren API-Schlüssel",
|
"invalid_auth": "Authentifizierung fehlgeschlagen - überprüfen Sie Ihren API-Schlüssel",
|
||||||
"invalid_api_key": "Ungültiger API-Schlüssel - bitte überprüfen Sie Ihre Anmeldedaten",
|
"invalid_api_key": "Ungültiger API-Schlüssel - überprüfen Sie Ihre Anmeldedaten",
|
||||||
"cannot_connect": "Verbindung zum API-Dienst fehlgeschlagen",
|
"cannot_connect": "Verbindung zum API-Dienst fehlgeschlagen",
|
||||||
"invalid_model": "Ausgewähltes Modell ist nicht verfügbar",
|
"invalid_model": "Das ausgewählte Modell ist nicht verfügbar",
|
||||||
"rate_limit": "Anfragelimit überschritten",
|
"rate_limit": "Ratenlimit überschritten",
|
||||||
"context_length": "Kontextlänge überschritten",
|
"context_length": "Kontextlänge überschritten",
|
||||||
"rate_limit_exceeded": "API-Anfragelimit überschritten",
|
"rate_limit_exceeded": "API-Ratenlimit überschritten",
|
||||||
"maintenance": "Dienst ist in Wartung",
|
"maintenance": "Dienst befindet sich in der Wartung",
|
||||||
"invalid_response": "Ungültige API-Antwort erhalten",
|
"invalid_response": "Ungültige API-Antwort empfangen",
|
||||||
"api_error": "API-Dienst-Fehler aufgetreten",
|
"api_error": "Fehler im API-Dienst aufgetreten",
|
||||||
"timeout": "Anfrage-Zeitüberschreitung",
|
"timeout": "Anfrage ist abgelaufen",
|
||||||
"invalid_instance": "Ungültige Instanz angegeben",
|
"invalid_instance": "Ungültige Instanz angegeben",
|
||||||
"unknown": "Unerwarteter Fehler aufgetreten"
|
"unknown": "Es ist ein unerwarteter Fehler aufgetreten",
|
||||||
|
"empty": "Name darf nicht leer sein",
|
||||||
|
"invalid_characters": "Name darf nur Buchstaben, Zahlen, Leerzeichen, Unterstriche und Bindestriche enthalten",
|
||||||
|
"name_too_long": "Name darf maximal 50 Zeichen lang sein"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"title": "Instanzeinstellungen aktualisieren",
|
"title": "Instanzeinstellungen aktualisieren",
|
||||||
"description": "Einstellungen für diese KI-Assistenten-Instanz ändern",
|
"description": "Ändern Sie die Einstellungen für diese KI-Assistenteninstanz.",
|
||||||
"data": {
|
"data": {
|
||||||
"model": "KI-Modell",
|
"model": "KI-Modell",
|
||||||
"temperature": "Antwort-Kreativität (0-2)",
|
"temperature": "Antwortkreativität (0-2)",
|
||||||
"max_tokens": "Maximale Antwortlänge (1-4096)",
|
"max_tokens": "Maximale Antwortlänge (1-4096)",
|
||||||
"request_interval": "Minimales Anfragen-Intervall (0,1-60 Sekunden)",
|
"request_interval": "Mindestzeitraum zwischen Anfragen (0,1-60 Sekunden)",
|
||||||
"context_messages": "Anzahl der vorherigen Nachrichten, die in den Kontext einbezogen werden sollen (1-20)"
|
"context_messages": "Anzahl der vorherigen Nachrichten, die im Kontext enthalten sein sollen (1-20)",
|
||||||
|
"max_history_size": "Maximale Größe des Konversationsverlaufs (1-100)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,31 +66,31 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Frage stellen (HA Text AI)",
|
"name": "Frage stellen (HA Text AI)",
|
||||||
"description": "Senden Sie eine Frage an das KI-Modell und erhalten Sie eine detaillierte Antwort. Die Antwort wird in der Gesprächshistorie gespeichert und kann später abgerufen werden.",
|
"description": "Senden Sie eine Frage an das KI-Modell und erhalten Sie eine detaillierte Antwort. Die Antwort wird im Konversationsverlauf gespeichert und kann später abgerufen werden.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Instanz",
|
"name": "Instanz",
|
||||||
"description": "Name der zu verwendenden HA Text AI Instanz"
|
"description": "Name der zu verwendenden HA Text AI-Instanz"
|
||||||
},
|
},
|
||||||
"question": {
|
"question": {
|
||||||
"name": "Frage",
|
"name": "Frage",
|
||||||
"description": "Ihre Frage oder Eingabeaufforderung für den KI-Assistenten"
|
"description": "Ihre Frage oder Aufforderung an den KI-Assistenten"
|
||||||
},
|
},
|
||||||
"context_messages": {
|
"context_messages": {
|
||||||
"name": "Kontextnachrichten",
|
"name": "Kontextnachrichten",
|
||||||
"description": "Anzahl der vorherigen Nachrichten, die in den Kontext einbezogen werden sollen (1-20)"
|
"description": "Anzahl der vorherigen Nachrichten, die im Kontext enthalten sein sollen (1-20)"
|
||||||
},
|
},
|
||||||
"system_prompt": {
|
"system_prompt": {
|
||||||
"name": "Systemaufforderung",
|
"name": "Systemprompt",
|
||||||
"description": "Optionale Systemaufforderung zur Kontexteinstellung für diese spezifische Frage"
|
"description": "Optionaler Systemprompt, um den Kontext für diese spezielle Frage festzulegen"
|
||||||
},
|
},
|
||||||
"model": {
|
"model": {
|
||||||
"name": "Modell",
|
"name": "Modell",
|
||||||
"description": "Wählen Sie das zu verwendende KI-Modell (optional, überschreibt Standardeinstellung)"
|
"description": "Wählen Sie das zu verwendende KI-Modell aus (optional, überschreibt die Standardeinstellung)"
|
||||||
},
|
},
|
||||||
"temperature": {
|
"temperature": {
|
||||||
"name": "Temperatur",
|
"name": "Temperatur",
|
||||||
"description": "Steuert die Antwort-Kreativität (0,0-2,0)"
|
"description": "Steuert die Antwortkreativität (0.0-2.0)"
|
||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Max. Token",
|
"name": "Max. Token",
|
||||||
@@ -95,54 +100,54 @@
|
|||||||
},
|
},
|
||||||
"clear_history": {
|
"clear_history": {
|
||||||
"name": "Verlauf löschen",
|
"name": "Verlauf löschen",
|
||||||
"description": "Alle gespeicherten Fragen und Antworten aus dem Gesprächsverlauf löschen",
|
"description": "Alle gespeicherten Fragen und Antworten aus dem Konversationsverlauf löschen",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Instanz",
|
"name": "Instanz",
|
||||||
"description": "Name der HA Text AI Instanz, für die der Verlauf gelöscht werden soll"
|
"description": "Name der HA Text AI-Instanz, deren Verlauf gelöscht werden soll"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"get_history": {
|
"get_history": {
|
||||||
"name": "Verlauf abrufen",
|
"name": "Verlauf abrufen",
|
||||||
"description": "Gesprächsverlauf mit optionaler Filterung und Sortierung abrufen",
|
"description": "Konversationsverlauf mit optionalem Filtern und Sortieren abrufen",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Instanz",
|
"name": "Instanz",
|
||||||
"description": "Name der HA Text AI Instanz, aus der der Verlauf abgerufen werden soll"
|
"description": "Name der HA Text AI-Instanz, von der der Verlauf abgerufen werden soll"
|
||||||
},
|
},
|
||||||
"limit": {
|
"limit": {
|
||||||
"name": "Limit",
|
"name": "Limit",
|
||||||
"description": "Anzahl der zurückzugebenden Gespräche (1-100)"
|
"description": "Anzahl der zurückzugebenden Konversationen (1-100)"
|
||||||
},
|
},
|
||||||
"filter_model": {
|
"filter_model": {
|
||||||
"name": "Modell filtern",
|
"name": "Modell filtern",
|
||||||
"description": "Gespräche nach bestimmtem KI-Modell filtern"
|
"description": "Konversationen nach einem bestimmten KI-Modell filtern"
|
||||||
},
|
},
|
||||||
"start_date": {
|
"start_date": {
|
||||||
"name": "Startdatum",
|
"name": "Startdatum",
|
||||||
"description": "Gespräche ab diesem Datum/Zeitpunkt filtern"
|
"description": "Konversationen ab diesem Datum/Uhrzeit filtern"
|
||||||
},
|
},
|
||||||
"include_metadata": {
|
"include_metadata": {
|
||||||
"name": "Metadaten einbeziehen",
|
"name": "Metadaten einschließen",
|
||||||
"description": "Zusätzliche Informationen wie verwendete Token, Antwortzeit usw. einbeziehen"
|
"description": "Zusätzliche Informationen wie verwendete Token, Antwortzeit usw. einschließen"
|
||||||
},
|
},
|
||||||
"sort_order": {
|
"sort_order": {
|
||||||
"name": "Sortierreihenfolge",
|
"name": "Sortierreihenfolge",
|
||||||
"description": "Sortierreihenfolge der Ergebnisse (neueste oder älteste zuerst)"
|
"description": "Sortierreihenfolge für die Ergebnisse (neueste oder älteste zuerst)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"set_system_prompt": {
|
"set_system_prompt": {
|
||||||
"name": "Systemaufforderung festlegen",
|
"name": "Systemprompt festlegen",
|
||||||
"description": "Standardmäßige Systemverhaltensinstruktionen für alle zukünftigen Gespräche festlegen",
|
"description": "Legen Sie Standardanweisungen für das Systemverhalten für alle zukünftigen Konversationen fest",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Instanz",
|
"name": "Instanz",
|
||||||
"description": "Name der HA Text AI Instanz, für die die Systemaufforderung festgelegt werden soll"
|
"description": "Name der HA Text AI-Instanz, für die der Systemprompt festgelegt werden soll"
|
||||||
},
|
},
|
||||||
"prompt": {
|
"prompt": {
|
||||||
"name": "Systemaufforderung",
|
"name": "Systemprompt",
|
||||||
"description": "Anweisungen, die definieren, wie sich die KI verhalten und antworten soll"
|
"description": "Anweisungen, die definieren, wie sich die KI verhalten und antworten soll"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,11 +162,11 @@
|
|||||||
"processing": "Verarbeitung",
|
"processing": "Verarbeitung",
|
||||||
"error": "Fehler",
|
"error": "Fehler",
|
||||||
"disconnected": "Getrennt",
|
"disconnected": "Getrennt",
|
||||||
"rate_limited": "Anfragelimit",
|
"rate_limited": "Ratenlimit",
|
||||||
"maintenance": "Wartung",
|
"maintenance": "Wartung",
|
||||||
"initializing": "Initialisierung",
|
"initializing": "Initialisierung",
|
||||||
"retrying": "Wiederholung",
|
"retrying": "Wiederholen",
|
||||||
"queued": "In Warteschlange"
|
"queued": "Warteschlange"
|
||||||
},
|
},
|
||||||
"state_attributes": {
|
"state_attributes": {
|
||||||
"question": {
|
"question": {
|
||||||
@@ -180,13 +185,13 @@
|
|||||||
"name": "Max. Token"
|
"name": "Max. Token"
|
||||||
},
|
},
|
||||||
"system_prompt": {
|
"system_prompt": {
|
||||||
"name": "Systemaufforderung"
|
"name": "Systemprompt"
|
||||||
},
|
},
|
||||||
"response_time": {
|
"response_time": {
|
||||||
"name": "Letzte Antwortzeit"
|
"name": "Letzte Antwortzeit"
|
||||||
},
|
},
|
||||||
"total_responses": {
|
"total_responses": {
|
||||||
"name": "Gesamte Antworten"
|
"name": "Gesamtzahl der Antworten"
|
||||||
},
|
},
|
||||||
"error_count": {
|
"error_count": {
|
||||||
"name": "Fehleranzahl"
|
"name": "Fehleranzahl"
|
||||||
@@ -198,19 +203,19 @@
|
|||||||
"name": "API-Status"
|
"name": "API-Status"
|
||||||
},
|
},
|
||||||
"tokens_used": {
|
"tokens_used": {
|
||||||
"name": "Insgesamt verwendete Token"
|
"name": "Verwendete Token insgesamt"
|
||||||
},
|
},
|
||||||
"average_response_time": {
|
"average_response_time": {
|
||||||
"name": "Durchschnittliche Antwortzeit"
|
"name": "Durchschnittliche Antwortzeit"
|
||||||
},
|
},
|
||||||
"last_request_time": {
|
"last_request_time": {
|
||||||
"name": "Letzte Anforderungszeit"
|
"name": "Zeitpunkt der letzten Anfrage"
|
||||||
},
|
},
|
||||||
"is_processing": {
|
"is_processing": {
|
||||||
"name": "Verarbeitungsstatus"
|
"name": "Verarbeitungsstatus"
|
||||||
},
|
},
|
||||||
"is_rate_limited": {
|
"is_rate_limited": {
|
||||||
"name": "Status Anfragelimit"
|
"name": "Ratenlimit-Status"
|
||||||
},
|
},
|
||||||
"is_maintenance": {
|
"is_maintenance": {
|
||||||
"name": "Wartungsstatus"
|
"name": "Wartungsstatus"
|
||||||
@@ -219,7 +224,7 @@
|
|||||||
"name": "API-Version"
|
"name": "API-Version"
|
||||||
},
|
},
|
||||||
"endpoint_status": {
|
"endpoint_status": {
|
||||||
"name": "Endpunktstatus"
|
"name": "Endpunkt-Status"
|
||||||
},
|
},
|
||||||
"performance_metrics": {
|
"performance_metrics": {
|
||||||
"name": "Leistungsmetriken"
|
"name": "Leistungsmetriken"
|
||||||
@@ -231,13 +236,13 @@
|
|||||||
"name": "Betriebszeit"
|
"name": "Betriebszeit"
|
||||||
},
|
},
|
||||||
"total_tokens": {
|
"total_tokens": {
|
||||||
"name": "Gesamte Token"
|
"name": "Gesamtzahl der Token"
|
||||||
},
|
},
|
||||||
"prompt_tokens": {
|
"prompt_tokens": {
|
||||||
"name": "Prompt-Token"
|
"name": "Prompt-Token"
|
||||||
},
|
},
|
||||||
"completion_tokens": {
|
"completion_tokens": {
|
||||||
"name": "Abschluss-Token"
|
"name": "Completion-Token"
|
||||||
},
|
},
|
||||||
"successful_requests": {
|
"successful_requests": {
|
||||||
"name": "Erfolgreiche Anfragen"
|
"name": "Erfolgreiche Anfragen"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"provider": {
|
"provider": {
|
||||||
"title": "Select AI Provider",
|
"title": "Select AI Provider",
|
||||||
"description": "Choose which AI service provider to use for this instance",
|
"description": "Choose which AI service provider to use for this instance.",
|
||||||
"data": {
|
"data": {
|
||||||
"api_provider": "API Provider",
|
"api_provider": "API Provider",
|
||||||
"context_messages": "Number of context messages to retain (1-20)"
|
"context_messages": "Number of context messages to retain (1-20)"
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Configure HA Text AI Instance",
|
"title": "Configure HA Text AI Instance",
|
||||||
"description": "Set up a new AI assistant instance with your selected provider",
|
"description": "Set up a new AI assistant instance with your selected provider.",
|
||||||
"data": {
|
"data": {
|
||||||
"name": "Instance name (e.g., 'GPT Assistant', 'Claude Helper')",
|
"name": "Instance name (e.g., 'GPT Assistant', 'Claude Helper')",
|
||||||
"api_key": "API key for authentication",
|
"api_key": "API key for authentication",
|
||||||
@@ -19,8 +19,10 @@
|
|||||||
"temperature": "Response creativity (0-2, lower = more focused)",
|
"temperature": "Response creativity (0-2, lower = more focused)",
|
||||||
"max_tokens": "Maximum response length (1-4096 tokens)",
|
"max_tokens": "Maximum response length (1-4096 tokens)",
|
||||||
"api_endpoint": "Custom API endpoint URL (optional)",
|
"api_endpoint": "Custom API endpoint URL (optional)",
|
||||||
|
"api_provider": "API Provider",
|
||||||
"request_interval": "Minimum time between requests (0.1-60 seconds)",
|
"request_interval": "Minimum time between requests (0.1-60 seconds)",
|
||||||
"context_messages": "Number of context messages to retain (1-20)"
|
"context_messages": "Number of context messages to retain (1-20)",
|
||||||
|
"max_history_size": "Maximum conversation history size (1-100)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -39,20 +41,24 @@
|
|||||||
"api_error": "API service error occurred",
|
"api_error": "API service error occurred",
|
||||||
"timeout": "Request timed out",
|
"timeout": "Request timed out",
|
||||||
"invalid_instance": "Invalid instance specified",
|
"invalid_instance": "Invalid instance specified",
|
||||||
"unknown": "Unexpected error occurred"
|
"unknown": "Unexpected error occurred",
|
||||||
|
"empty": "Name cannot be empty",
|
||||||
|
"invalid_characters": "Name can only contain letters, numbers, spaces, underscores and hyphens",
|
||||||
|
"name_too_long": "Name must be 50 characters or less"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"title": "Update Instance Settings",
|
"title": "Update Instance Settings",
|
||||||
"description": "Modify settings for this AI assistant instance",
|
"description": "Modify settings for this AI assistant instance.",
|
||||||
"data": {
|
"data": {
|
||||||
"model": "AI model",
|
"model": "AI model",
|
||||||
"temperature": "Response creativity (0-2)",
|
"temperature": "Response creativity (0-2)",
|
||||||
"max_tokens": "Maximum response length (1-4096)",
|
"max_tokens": "Maximum response length (1-4096)",
|
||||||
"request_interval": "Minimum request interval (0.1-60 seconds)",
|
"request_interval": "Minimum request interval (0.1-60 seconds)",
|
||||||
"context_messages": "Number of previous messages to include in context (1-20)"
|
"context_messages": "Number of previous messages to include in context (1-20)",
|
||||||
|
"max_history_size": "Maximum conversation history size (1-100)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,57 +2,63 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"provider": {
|
"provider": {
|
||||||
"title": "Выбор провайдера ИИ",
|
"title": "Выбор поставщика ИИ",
|
||||||
"description": "Выберите сервис ИИ для этого экземпляра",
|
"description": "Выберите поставщика услуг ИИ для этой инстанции.",
|
||||||
"data": {
|
"data": {
|
||||||
"api_provider": "Провайдер API",
|
"api_provider": "Поставщик API",
|
||||||
"context_messages": "Количество сообщений в контексте (1-20)"
|
"context_messages": "Количество контекстных сообщений для сохранения (1-20)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Настройка экземпляра HA Text AI",
|
"title": "Настройка инстанции HA Text AI",
|
||||||
"description": "Настройте нового помощника ИИ с выбранным провайдером",
|
"description": "Настройте новую инстанцию помощника ИИ с выбранным вами поставщиком.",
|
||||||
"data": {
|
"data": {
|
||||||
"name": "Название экземпляра (например, 'GPT Помощник', 'Claude Ассистент')",
|
"name": "Имя инстанции (например, 'GPT Assistant', 'Claude Helper')",
|
||||||
"api_key": "API-ключ для аутентификации",
|
"api_key": "API-ключ для аутентификации",
|
||||||
"model": "Модель ИИ для использования",
|
"model": "Используемая модель ИИ",
|
||||||
"temperature": "Креативность ответов (0-2, меньше = более сфокусированно)",
|
"temperature": "Креативность ответа (0-2, чем ниже, тем больше фокусировки)",
|
||||||
"max_tokens": "Максимальная длина ответа (1-4096 токенов)",
|
"max_tokens": "Максимальная длина ответа (1-4096 токенов)",
|
||||||
"api_endpoint": "Пользовательский URL-адрес API (необязательно)",
|
"api_endpoint": "Пользовательский URL-адрес конечной точки API (необязательно)",
|
||||||
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
"api_provider": "Поставщик API",
|
||||||
"context_messages": "Количество сохраняемых контекстных сообщений (1-20)"
|
"request_interval": "Минимальное время между запросами (0,1-60 секунд)",
|
||||||
|
"context_messages": "Количество контекстных сообщений для сохранения (1-20)",
|
||||||
|
"max_history_size": "Максимальный размер истории разговора (1-100)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"name_exists": "Экземпляр с таким именем уже существует",
|
"name_exists": "Инстанция с таким именем уже существует",
|
||||||
"invalid_name": "Некорректное имя экземпляра",
|
"invalid_name": "Некорректное имя инстанции",
|
||||||
"invalid_auth": "Ошибка аутентификации - проверьте API-ключ",
|
"invalid_auth": "Аутентификация не удалась - проверьте ваш API-ключ",
|
||||||
"invalid_api_key": "Неверный API-ключ - проверьте учетные данные",
|
"invalid_api_key": "Неверный API-ключ - проверьте ваши учетные данные",
|
||||||
"cannot_connect": "Не удалось подключиться к сервису API",
|
"cannot_connect": "Не удалось подключиться к службе API",
|
||||||
"invalid_model": "Выбранная модель недоступна",
|
"invalid_model": "Выбранная модель недоступна",
|
||||||
"rate_limit": "Превышен лимит запросов",
|
"rate_limit": "Превышен лимит запросов",
|
||||||
"context_length": "Превышена длина контекста",
|
"context_length": "Превышена длина контекста",
|
||||||
"rate_limit_exceeded": "Превышен лимит API",
|
"rate_limit_exceeded": "Превышен лимит запросов API",
|
||||||
"maintenance": "Сервис на техническом обслуживании",
|
"maintenance": "Сервис находится на техническом обслуживании",
|
||||||
"invalid_response": "Получен некорректный ответ API",
|
"invalid_response": "Получен неверный ответ API",
|
||||||
"api_error": "Произошла ошибка сервиса API",
|
"api_error": "Произошла ошибка службы API",
|
||||||
"timeout": "Время ожидания истекло",
|
"timeout": "Запрос превысил время ожидания",
|
||||||
"invalid_instance": "Указан неверный экземпляр",
|
"invalid_instance": "Указана неверная инстанция",
|
||||||
"unknown": "Произошла непредвиденная ошибка"
|
"unknown": "Произошла непредвиденная ошибка",
|
||||||
|
"empty": "Имя не может быть пустым",
|
||||||
|
"invalid_characters": "Имя может содержать только буквы, цифры, пробелы, подчеркивания и дефисы",
|
||||||
|
"name_too_long": "Имя должно быть не более 50 символов"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"title": "Обновление настроек экземпляра",
|
"title": "Обновление настроек инстанции",
|
||||||
"description": "Измените настройки для этого помощника ИИ",
|
"description": "Измените настройки для этой инстанции помощника ИИ.",
|
||||||
"data": {
|
"data": {
|
||||||
"model": "Модель ИИ",
|
"model": "Модель ИИ",
|
||||||
"temperature": "Креативность ответов (0-2)",
|
"temperature": "Креативность ответа (0-2)",
|
||||||
"max_tokens": "Максимальная длина ответа (1-4096)",
|
"max_tokens": "Максимальная длина ответа (1-4096)",
|
||||||
"request_interval": "Минимальный интервал запросов (0.1-60 секунд)",
|
"request_interval": "Минимальный интервал между запросами (0,1-60 секунд)",
|
||||||
"context_messages": "Количество предыдущих сообщений для включения в контекст (1-20)"
|
"context_messages": "Количество предыдущих сообщений для включения в контекст (1-20)",
|
||||||
|
"max_history_size": "Максимальный размер истории разговора (1-100)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,89 +66,89 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Задать вопрос (HA Text AI)",
|
"name": "Задать вопрос (HA Text AI)",
|
||||||
"description": "Отправьте вопрос модели ИИ и получите подробный ответ. Ответ будет сохранен в истории беседы и может быть извлечен позже.",
|
"description": "Отправьте вопрос модели ИИ и получите подробный ответ. Ответ будет сохранен в истории разговора и может быть извлечен позже.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Экземпляр",
|
"name": "Инстанция",
|
||||||
"description": "Название экземпляра HA Text AI для использования"
|
"description": "Имя используемой инстанции HA Text AI"
|
||||||
},
|
},
|
||||||
"question": {
|
"question": {
|
||||||
"name": "Вопрос",
|
"name": "Вопрос",
|
||||||
"description": "Ваш вопрос или запрос помощнику ИИ"
|
"description": "Ваш вопрос или запрос к помощнику ИИ"
|
||||||
},
|
},
|
||||||
"context_messages": {
|
"context_messages": {
|
||||||
"name": "Контекстные сообщения",
|
"name": "Контекстные сообщения",
|
||||||
"description": "Количество предыдущих сообщений для включения в контекст (1-20)"
|
"description": "Количество предыдущих сообщений для включения в контекст (1-20)"
|
||||||
},
|
},
|
||||||
"system_prompt": {
|
"system_prompt": {
|
||||||
"name": "Системный промпт",
|
"name": "Системный запрос",
|
||||||
"description": "Необязательный системный промпт для установки контекста для этого конкретного вопроса"
|
"description": "Необязательный системный запрос для установки контекста для этого конкретного вопроса"
|
||||||
},
|
},
|
||||||
"model": {
|
"model": {
|
||||||
"name": "Модель",
|
"name": "Модель",
|
||||||
"description": "Выберите модель ИИ (необязательно, переопределяет настройки по умолчанию)"
|
"description": "Выберите модель ИИ для использования (необязательно, переопределяет настройку по умолчанию)"
|
||||||
},
|
},
|
||||||
"temperature": {
|
"temperature": {
|
||||||
"name": "Температура",
|
"name": "Температура",
|
||||||
"description": "Управляет креативностью ответа (0.0-2.0)"
|
"description": "Управляет креативностью ответа (0.0-2.0)"
|
||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Максимальное количество токенов",
|
"name": "Макс. токенов",
|
||||||
"description": "Максимальная длина ответа (1-4096 токенов)"
|
"description": "Максимальная длина ответа (1-4096 токенов)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clear_history": {
|
"clear_history": {
|
||||||
"name": "Очистить историю",
|
"name": "Очистить историю",
|
||||||
"description": "Удалить все сохраненные вопросы и ответы из истории беседы",
|
"description": "Удалить все сохраненные вопросы и ответы из истории разговора",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Экземпляр",
|
"name": "Инстанция",
|
||||||
"description": "Название экземпляра HA Text AI для очистки истории"
|
"description": "Имя инстанции HA Text AI, для которой нужно очистить историю"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"get_history": {
|
"get_history": {
|
||||||
"name": "Получить историю",
|
"name": "Получить историю",
|
||||||
"description": "Получить историю беседы с дополнительной фильтрацией и сортировкой",
|
"description": "Извлечь историю разговора с возможностью фильтрации и сортировки",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Экземпляр",
|
"name": "Инстанция",
|
||||||
"description": "Название экземпляра HA Text AI для получения истории"
|
"description": "Имя инстанции HA Text AI, из которой нужно получить историю"
|
||||||
},
|
},
|
||||||
"limit": {
|
"limit": {
|
||||||
"name": "Лимит",
|
"name": "Лимит",
|
||||||
"description": "Количество возвращаемых бесед (1-100)"
|
"description": "Количество разговоров для возврата (1-100)"
|
||||||
},
|
},
|
||||||
"filter_model": {
|
"filter_model": {
|
||||||
"name": "Фильтр модели",
|
"name": "Фильтр модели",
|
||||||
"description": "Фильтрация бесед по конкретной модели ИИ"
|
"description": "Фильтрация разговоров по определенной модели ИИ"
|
||||||
},
|
},
|
||||||
"start_date": {
|
"start_date": {
|
||||||
"name": "Начальная дата",
|
"name": "Дата начала",
|
||||||
"description": "Фильтрация бесед, начиная с указанной даты/времени"
|
"description": "Фильтрация разговоров, начиная с этой даты/времени"
|
||||||
},
|
},
|
||||||
"include_metadata": {
|
"include_metadata": {
|
||||||
"name": "Включить метаданные",
|
"name": "Включить метаданные",
|
||||||
"description": "Включить дополнительную информацию, например, использованные токены, время ответа и т.д."
|
"description": "Включить дополнительную информацию, такую как использованные токены, время ответа и т.д."
|
||||||
},
|
},
|
||||||
"sort_order": {
|
"sort_order": {
|
||||||
"name": "Порядок сортировки",
|
"name": "Порядок сортировки",
|
||||||
"description": "Порядок результатов (сначала новые или старые)"
|
"description": "Порядок сортировки результатов (самые новые или самые старые)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"set_system_prompt": {
|
"set_system_prompt": {
|
||||||
"name": "Установить системный промпт",
|
"name": "Установить системный запрос",
|
||||||
"description": "Установить инструкции по умолчанию для поведения ИИ во всех будущих разговорах",
|
"description": "Установить инструкции по умолчанию для поведения системы для всех будущих разговоров",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Экземпляр",
|
"name": "Инстанция",
|
||||||
"description": "Название экземпляра HA Text AI для установки системного промпта"
|
"description": "Имя инстанции HA Text AI, для которой нужно установить системный запрос"
|
||||||
},
|
},
|
||||||
"prompt": {
|
"prompt": {
|
||||||
"name": "Системный промпт",
|
"name": "Системный запрос",
|
||||||
"description": "Инструкции, определяющие, как ИИ должен вести себя и отвечать"
|
"description": "Инструкции, определяющие, как ИИ должен себя вести и отвечать"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,10 +182,10 @@
|
|||||||
"name": "Температура"
|
"name": "Температура"
|
||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Максимальное количество токенов"
|
"name": "Макс. токенов"
|
||||||
},
|
},
|
||||||
"system_prompt": {
|
"system_prompt": {
|
||||||
"name": "Системный промпт"
|
"name": "Системный запрос"
|
||||||
},
|
},
|
||||||
"response_time": {
|
"response_time": {
|
||||||
"name": "Время последнего ответа"
|
"name": "Время последнего ответа"
|
||||||
@@ -197,7 +203,7 @@
|
|||||||
"name": "Статус API"
|
"name": "Статус API"
|
||||||
},
|
},
|
||||||
"tokens_used": {
|
"tokens_used": {
|
||||||
"name": "Всего использовано токенов"
|
"name": "Использовано токенов всего"
|
||||||
},
|
},
|
||||||
"average_response_time": {
|
"average_response_time": {
|
||||||
"name": "Среднее время ответа"
|
"name": "Среднее время ответа"
|
||||||
@@ -209,19 +215,19 @@
|
|||||||
"name": "Статус обработки"
|
"name": "Статус обработки"
|
||||||
},
|
},
|
||||||
"is_rate_limited": {
|
"is_rate_limited": {
|
||||||
"name": "Статус лимита запросов"
|
"name": "Статус ограничения запросов"
|
||||||
},
|
},
|
||||||
"is_maintenance": {
|
"is_maintenance": {
|
||||||
"name": "Статус обслуживания"
|
"name": "Статус технического обслуживания"
|
||||||
},
|
},
|
||||||
"api_version": {
|
"api_version": {
|
||||||
"name": "Версия API"
|
"name": "Версия API"
|
||||||
},
|
},
|
||||||
"endpoint_status": {
|
"endpoint_status": {
|
||||||
"name": "Статус эндпоинта"
|
"name": "Статус конечной точки"
|
||||||
},
|
},
|
||||||
"performance_metrics": {
|
"performance_metrics": {
|
||||||
"name": "Показатели производительности"
|
"name": "Метрики производительности"
|
||||||
},
|
},
|
||||||
"history_size": {
|
"history_size": {
|
||||||
"name": "Размер истории"
|
"name": "Размер истории"
|
||||||
@@ -233,16 +239,16 @@
|
|||||||
"name": "Всего токенов"
|
"name": "Всего токенов"
|
||||||
},
|
},
|
||||||
"prompt_tokens": {
|
"prompt_tokens": {
|
||||||
"name": "Токены промпта"
|
"name": "Токены запроса"
|
||||||
},
|
},
|
||||||
"completion_tokens": {
|
"completion_tokens": {
|
||||||
"name": "Токены завершения"
|
"name": "Токены завершения"
|
||||||
},
|
},
|
||||||
"successful_requests": {
|
"successful_requests": {
|
||||||
"name": "Успешные запросы"
|
"name": "Успешных запросов"
|
||||||
},
|
},
|
||||||
"failed_requests": {
|
"failed_requests": {
|
||||||
"name": "Неудачные запросы"
|
"name": "Неудачных запросов"
|
||||||
},
|
},
|
||||||
"average_latency": {
|
"average_latency": {
|
||||||
"name": "Средняя задержка"
|
"name": "Средняя задержка"
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "HA text AI",
|
"name": "HA text AI",
|
||||||
"render_readme": true,
|
"render_readme": true,
|
||||||
"icon": "mdi:robot",
|
"homeassistant": "2024.11.0"
|
||||||
"domains": ["sensor"],
|
|
||||||
"homeassistant": "2024.11.0",
|
|
||||||
"version": "2.0.0",
|
|
||||||
"documentation": "https://github.com/smkrv/ha-text-ai"
|
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 923 KiB |
+20
-14
@@ -2,18 +2,24 @@
|
|||||||
ha-text-ai/
|
ha-text-ai/
|
||||||
│
|
│
|
||||||
├── custom_components/
|
├── custom_components/
|
||||||
│ └── ha_text_ai/
|
├── ha_text_ai/
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
│ ├── config_flow.py
|
│ ├── config_flow.py
|
||||||
│ ├── coordinator.py
|
│ ├── coordinator.py
|
||||||
│ ├── manifest.json
|
│ ├── manifest.json
|
||||||
│ ├── sensor.py
|
│ ├── sensor.py
|
||||||
│ ├── services.yaml
|
│ ├── services.yaml
|
||||||
│ ├── const.py
|
│ ├── const.py
|
||||||
│ └── api_client.py
|
│ └── api_client.py
|
||||||
│
|
│
|
||||||
└── strings/
|
├── translations/
|
||||||
├── en.json
|
│ ├── en.json
|
||||||
├── de.json
|
│ ├── de.json
|
||||||
└── ru.json
|
│ └── ru.json
|
||||||
|
│
|
||||||
|
└── icons/
|
||||||
|
├── icon.png
|
||||||
|
├── icon@2x.png
|
||||||
|
├── logo.png
|
||||||
|
└── logo@2x.png
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user