mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-22 23:24:03 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eee9754033 | ||
|
|
517b1f11ae | ||
|
|
ed8f19bfa9 | ||
|
|
7e3daf611b | ||
|
|
37919be70f | ||
|
|
e427254584 | ||
|
|
76c5629fa0 | ||
|
|
7958bd010b | ||
|
|
8cd876195a | ||
|
|
376753e001 | ||
|
|
b6e73e847d | ||
|
|
440c734214 | ||
|
|
73788373cd | ||
|
|
4bfc96019b | ||
|
|
2138fc7654 | ||
|
|
95bd2ebb41 | ||
|
|
cad0fd7031 | ||
|
|
c003b258f6 | ||
|
|
65a10c77f4 | ||
|
|
e1463828c9 | ||
|
|
5ebb9c9c66 | ||
|
|
f17c631a79 | ||
|
|
0e06794384 | ||
|
|
d8a924909b | ||
|
|
29f1659a02 | ||
|
|
5b7905de80 | ||
|
|
cf9ac6dcea | ||
|
|
568eb3e16c | ||
|
|
53fb150389 | ||
|
|
acbb53d2af | ||
|
|
e19db29441 | ||
|
|
bfd64d1122 | ||
|
|
82e1f0c4f9 | ||
|
|
5c16eee6e4 | ||
|
|
e988d445a4 | ||
|
|
f9bfb9ab7f | ||
|
|
92dd1bc110 | ||
|
|
17d547325a | ||
|
|
b8cb70217c | ||
|
|
530d04f25d | ||
|
|
b71083b9bf | ||
|
|
f9f7d10f7f | ||
|
|
9f7cb20621 | ||
|
|
6fc3b23365 | ||
|
|
15c717fcb0 | ||
|
|
be06fddce1 | ||
|
|
5f0bd861a7 | ||
|
|
428aee46c8 | ||
|
|
561bcf0b1d | ||
|
|
2b1e42c665 |
-40
@@ -1,40 +0,0 @@
|
|||||||
# Python
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
*.so
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
|
|
||||||
# Home Assistant
|
|
||||||
.storage
|
|
||||||
.cloud
|
|
||||||
.google.token
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
*.psd
|
|
||||||
*.zip
|
|
||||||
*.txt
|
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
issue tracker.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
@@ -2,30 +2,33 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
  [](https://creativecommons.org/licenses/by-nc-sa/4.0/) [](https://github.com/hacs/integration)   
|
  [](https://creativecommons.org/licenses/by-nc-sa/4.0/) [](https://github.com/hacs/integration)
|
||||||
|
       
|
||||||
|
|
||||||
<img src="https://github.com/smkrv/ha-text-ai/blob/2aaf3405759eb2d97624834594e24ace896131df/assets/images/icons/logo.png" alt="HA Text AI" style="width: 80%; max-width: 640px; max-height: 150px; aspect-ratio: 2/1; object-fit: contain;"/>
|
|
||||||
|
<img src="https://github.com/smkrv/ha-text-ai/blob/15c717fcb0204bf4a0d4b4b4c6f0bb93e9f6c9a9/custom_components/ha_text_ai/icons/logo%402x.png" alt="HA Text AI" style="width: 50%; max-width: 256px; max-height: 128px; aspect-ratio: 2/1; object-fit: contain;"/>
|
||||||
|
|
||||||
### Advanced AI Integration for [Home Assistant](https://www.home-assistant.io/) with LLM multi-provider support
|
### Advanced AI Integration for [Home Assistant](https://www.home-assistant.io/) 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, DeepSeek and Anthropic Claude models. Get intelligent responses, automate complex scenarios, and enhance your home automation with advanced natural language processing.
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> 🤝 Community Driven
|
> 🤝 Community Driven: for more details on the integration,
|
||||||
|
> check out the discussion on the **[Home Assistant Community forum](https://community.home-assistant.io/t/ha-text-ai-transforming-home-automation-through-multi-llm-integration/799741)**
|
||||||
>
|
>
|
||||||
> <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>
|
> <a href="https://my.home-assistant.io/redirect/hacs_repository/?owner=smkrv&repository=ha-text-ai&category=Integration"><img src="https://my.home-assistant.io/badges/hacs_repository.svg" width="210" height="auto"></a>
|
||||||
>
|
>
|
||||||
> [Screenshots](assets/images/screenshots/screenshot.jpg)
|
> [Screenshots](assets/images/screenshots/screenshot.jpg)
|
||||||
|
|
||||||
## 🌟 Features
|
## 🌟 Features
|
||||||
|
|
||||||
- 🧠 **Multi-Provider AI Integration**: Support for OpenAI GPT and Anthropic Claude models
|
- 🧠 **Multi-Provider AI Integration**: Support for OpenAI GPT, DeepSeek and Anthropic Claude models
|
||||||
- 💬 **Advanced Language Processing**: Context-aware, multi-turn conversations
|
- 💬 **Advanced Language Processing**: Context-aware, multi-turn conversations
|
||||||
- 📝 **Enhanced Memory Management**: Secure file-based history storage
|
- 📝 **Enhanced Memory Management**: Secure file-based history storage
|
||||||
- ⚡ **Performance Optimization**: Efficient token usage and smart rate limiting
|
- ⚡ **Performance Optimization**: Efficient token usage and smart rate limiting
|
||||||
@@ -40,6 +43,7 @@ Transform your smart home experience with powerful AI assistance powered by mult
|
|||||||
### 🧠 **Multi-Provider AI Integration**
|
### 🧠 **Multi-Provider AI Integration**
|
||||||
- Support for OpenAI GPT models
|
- Support for OpenAI GPT models
|
||||||
- Anthropic Claude integration
|
- Anthropic Claude integration
|
||||||
|
- DeepSeek integration
|
||||||
- Custom API endpoints
|
- Custom API endpoints
|
||||||
- Flexible model selection
|
- Flexible model selection
|
||||||
|
|
||||||
@@ -102,11 +106,13 @@ Transform your smart home experience with powerful AI assistance powered by mult
|
|||||||
|
|
||||||
## 📋 Prerequisites
|
## 📋 Prerequisites
|
||||||
|
|
||||||
- Home Assistant 2024.11 or later
|
- Home Assistant 2024.2.2 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/))
|
||||||
|
- DeepSeek ([Get key](https://platform.deepseek.com/api_keys))
|
||||||
- OpenRouter ([Get key](https://openrouter.ai/keys))
|
- OpenRouter ([Get key](https://openrouter.ai/keys))
|
||||||
|
- Google Gemini 🆕 ([Get key](https://ai.google.dev/gemini-api/docs/api-key)) thanks to ([@Azzedde](https://github.com/Azzedde))
|
||||||
- Any OpenAI-compatible API provider
|
- Any OpenAI-compatible API provider
|
||||||
- Python 3.9 or newer
|
- Python 3.9 or newer
|
||||||
- Stable internet connection
|
- Stable internet connection
|
||||||
@@ -114,11 +120,11 @@ Transform your smart home experience with powerful AI assistance powered by mult
|
|||||||
## Configuration Options
|
## Configuration Options
|
||||||
|
|
||||||
### 🔧 **Core Configuration Settings**
|
### 🔧 **Core Configuration Settings**
|
||||||
- 🌐 **API Provider**: OpenAI/Anthropic
|
- 🌐 **API Provider**: OpenAI/Anthropic/DeepSeek
|
||||||
- 🔑 **API Key**: Provider-specific authentication
|
- 🔑 **API Key**: Provider-specific authentication
|
||||||
- 🤖 **Model Selection**: Flexible, provider-specific models
|
- 🤖 **Model Selection**: Flexible, provider-specific models
|
||||||
- 🌡️ **Temperature**: Creativity control (0.0-2.0)
|
- 🌡️ **Temperature**: Creativity control (0.0-2.0)
|
||||||
- 📏 **Max Tokens**: Response length limit (token usage is estimated using a heuristic method based on word count and specific word characteristics, which may differ from actual token usage)
|
- 📏 **Max Tokens**: Response length limit (passed directly to the LLM API to control the maximum length of the response)
|
||||||
- ⏱️ **Request Interval**: API call throttling
|
- ⏱️ **Request Interval**: API call throttling
|
||||||
- 💾 **History Size**: Number of messages to retain
|
- 💾 **History Size**: Number of messages to retain
|
||||||
- 🌍 **Custom API Endpoint**: Optional advanced configuration
|
- 🌍 **Custom API Endpoint**: Optional advanced configuration
|
||||||
@@ -154,6 +160,9 @@ To be compatible, a provider should support:
|
|||||||
## ⚡ Installation
|
## ⚡ Installation
|
||||||
|
|
||||||
### HACS Installation (Recommended)
|
### HACS Installation (Recommended)
|
||||||
|
>[!TIP]
|
||||||
|
>HA Text AI is available in the default HACS repository. You can install it directly through HACS or click the button below to open it there.
|
||||||
|
|
||||||
<a href="https://my.home-assistant.io/redirect/hacs_repository/?owner=smkrv&repository=ha-text-ai&category=Integration"><img src="https://my.home-assistant.io/badges/hacs_repository.svg" width="170" height="auto"></a>
|
<a href="https://my.home-assistant.io/redirect/hacs_repository/?owner=smkrv&repository=ha-text-ai&category=Integration"><img src="https://my.home-assistant.io/badges/hacs_repository.svg" width="170" height="auto"></a>
|
||||||
1. Open HACS in Home Assistant
|
1. Open HACS in Home Assistant
|
||||||
2. Click on "Integrations"
|
2. Click on "Integrations"
|
||||||
@@ -241,6 +250,16 @@ sensor:
|
|||||||
|
|
||||||
## 🛠️ Available Services
|
## 🛠️ Available Services
|
||||||
|
|
||||||
|
### 🔄 Response Variables (New!)
|
||||||
|
|
||||||
|
**HA Text AI now supports response variables** - a powerful feature that returns AI responses directly from service calls, eliminating the need for separate text sensors and the 255-character limitation!
|
||||||
|
|
||||||
|
#### ✨ Key Benefits:
|
||||||
|
- **Unlimited response length** - No more 255-character truncation
|
||||||
|
- **Direct data access** - Get responses immediately in automations
|
||||||
|
- **Race condition prevention** - Eliminates conflicts in parallel automations
|
||||||
|
- **Simplified workflows** - No need to read from sensors
|
||||||
|
|
||||||
### ask_question
|
### ask_question
|
||||||
```yaml
|
```yaml
|
||||||
service: ha_text_ai.ask_question
|
service: ha_text_ai.ask_question
|
||||||
@@ -252,6 +271,22 @@ data:
|
|||||||
context_messages: 10 #optional, number of previous messages to include in context, default: 5
|
context_messages: 10 #optional, number of previous messages to include in context, default: 5
|
||||||
system_prompt: "You are a sleep optimization expert" # optional
|
system_prompt: "You are a sleep optimization expert" # optional
|
||||||
instance: sensor.ha_text_ai_gpt
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: ai_response # NEW! Store response data directly
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 📊 Response Data Structure:
|
||||||
|
```yaml
|
||||||
|
# The service returns structured data:
|
||||||
|
response_text: "The optimal sleeping temperature is 65-68°F (18-20°C)..."
|
||||||
|
tokens_used: 150
|
||||||
|
prompt_tokens: 50
|
||||||
|
completion_tokens: 100
|
||||||
|
model_used: "claude-3-sonnet"
|
||||||
|
instance: "sensor.ha_text_ai_gpt"
|
||||||
|
question: "What's the optimal temperature for sleeping?"
|
||||||
|
timestamp: "2025-01-09T16:57:00.000Z"
|
||||||
|
success: true
|
||||||
|
# error: "Error message" (only present if success: false)
|
||||||
```
|
```
|
||||||
|
|
||||||
### set_system_prompt
|
### set_system_prompt
|
||||||
@@ -270,6 +305,8 @@ data:
|
|||||||
### clear_history
|
### clear_history
|
||||||
```yaml
|
```yaml
|
||||||
service: ha_text_ai.clear_history
|
service: ha_text_ai.clear_history
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
```
|
```
|
||||||
|
|
||||||
### get_history
|
### get_history
|
||||||
@@ -278,6 +315,150 @@ service: ha_text_ai.get_history
|
|||||||
data:
|
data:
|
||||||
limit: 5 # optional
|
limit: 5 # optional
|
||||||
filter_model: "gpt-4o" # optional
|
filter_model: "gpt-4o" # optional
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Advanced Automation Examples with Response Variables
|
||||||
|
|
||||||
|
### Example 1: Smart Home Advice with Direct Response
|
||||||
|
```yaml
|
||||||
|
automation:
|
||||||
|
- alias: "Get AI Home Advice"
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: input_button.ask_ai_advice
|
||||||
|
action:
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: "What's the best way to optimize energy usage in my home?"
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: ai_advice
|
||||||
|
- service: notify.mobile_app
|
||||||
|
data:
|
||||||
|
title: "🏠 Smart Home Tip"
|
||||||
|
message: |
|
||||||
|
{{ ai_advice.response_text }}
|
||||||
|
|
||||||
|
📊 Tokens used: {{ ai_advice.tokens_used }}
|
||||||
|
🤖 Model: {{ ai_advice.model_used }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Weather-Based AI Recommendations
|
||||||
|
```yaml
|
||||||
|
automation:
|
||||||
|
- alias: "Weather-Based AI Suggestions"
|
||||||
|
trigger:
|
||||||
|
- platform: numeric_state
|
||||||
|
entity_id: sensor.outdoor_temperature
|
||||||
|
below: 0
|
||||||
|
action:
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: |
|
||||||
|
The outdoor temperature is {{ states('sensor.outdoor_temperature') }}°C.
|
||||||
|
What should I do to prepare my home for freezing weather?
|
||||||
|
system_prompt: "You are a home maintenance expert. Provide practical, actionable advice."
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: winter_advice
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ winter_advice.success }}"
|
||||||
|
then:
|
||||||
|
- service: persistent_notification.create
|
||||||
|
data:
|
||||||
|
title: "❄️ Winter Preparation Advice"
|
||||||
|
message: |
|
||||||
|
{{ winter_advice.response_text }}
|
||||||
|
|
||||||
|
Generated at: {{ winter_advice.timestamp }}
|
||||||
|
else:
|
||||||
|
- service: persistent_notification.create
|
||||||
|
data:
|
||||||
|
title: "⚠️ AI Service Error"
|
||||||
|
message: "Failed to get winter advice: {{ winter_advice.error }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Multi-Step AI Workflow
|
||||||
|
```yaml
|
||||||
|
automation:
|
||||||
|
- alias: "Multi-Step AI Analysis"
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: input_button.analyze_home_status
|
||||||
|
action:
|
||||||
|
# Step 1: Get current status analysis
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: |
|
||||||
|
Current home status:
|
||||||
|
- Temperature: {{ states('sensor.indoor_temperature') }}°C
|
||||||
|
- Humidity: {{ states('sensor.indoor_humidity') }}%
|
||||||
|
- Energy usage: {{ states('sensor.power_consumption') }}W
|
||||||
|
|
||||||
|
Analyze this data and provide insights.
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: status_analysis
|
||||||
|
|
||||||
|
# Step 2: Get recommendations based on analysis
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: |
|
||||||
|
Based on this analysis: "{{ status_analysis.response_text[:500] }}"
|
||||||
|
|
||||||
|
Provide 3 specific actionable recommendations for improvement.
|
||||||
|
context_messages: 2 # Include previous conversation
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: recommendations
|
||||||
|
|
||||||
|
# Step 3: Send comprehensive report
|
||||||
|
- service: notify.telegram
|
||||||
|
data:
|
||||||
|
title: "🏠 Home Analysis Report"
|
||||||
|
message: |
|
||||||
|
**Analysis:**
|
||||||
|
{{ status_analysis.response_text }}
|
||||||
|
|
||||||
|
**Recommendations:**
|
||||||
|
{{ recommendations.response_text }}
|
||||||
|
|
||||||
|
**Report Details:**
|
||||||
|
- Total tokens used: {{ status_analysis.tokens_used + recommendations.tokens_used }}
|
||||||
|
- Analysis model: {{ status_analysis.model_used }}
|
||||||
|
- Generated: {{ recommendations.timestamp }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 💡 Migration from Sensors to Response Variables
|
||||||
|
|
||||||
|
#### Old Method (Limited):
|
||||||
|
```yaml
|
||||||
|
# ❌ Old way - limited to 255 characters, race conditions
|
||||||
|
automation:
|
||||||
|
- alias: "Old AI Response Method"
|
||||||
|
action:
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: "Long question here..."
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
- delay: "00:00:05" # Wait for sensor update
|
||||||
|
- service: notify.mobile
|
||||||
|
data:
|
||||||
|
message: "{{ state_attr('sensor.ha_text_ai_gpt', 'response')[:255] }}..." # Truncated!
|
||||||
|
```
|
||||||
|
|
||||||
|
#### New Method (Unlimited):
|
||||||
|
```yaml
|
||||||
|
# ✅ New way - unlimited length, immediate access, no race conditions
|
||||||
|
automation:
|
||||||
|
- alias: "New AI Response Method"
|
||||||
|
action:
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: "Long question here..."
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: ai_response # Direct access!
|
||||||
|
- service: notify.mobile
|
||||||
|
data:
|
||||||
|
message: "{{ ai_response.response_text }}" # Full response, no truncation!
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🏷️ HA Text AI Sensor Naming Convention
|
### 🏷️ HA Text AI Sensor Naming Convention
|
||||||
@@ -400,7 +581,7 @@ automation:
|
|||||||
# Number of entries in current history file
|
# Number of entries in current history file
|
||||||
{{ state_attr('sensor.ha_text_ai_gpt', 'History size') }} # 0
|
{{ state_attr('sensor.ha_text_ai_gpt', 'History size') }} # 0
|
||||||
|
|
||||||
# Last few conversation entries (limited to 3 for performance)
|
# Last few conversation entries (limited to 1 for performance)
|
||||||
{{ state_attr('sensor.ha_text_ai_gpt', 'conversation_history') }} # [...]
|
{{ state_attr('sensor.ha_text_ai_gpt', 'conversation_history') }} # [...]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# Using response_variable with HA Text AI
|
||||||
|
|
||||||
|
After updating the HA Text AI integration, it now supports using the `response_variable` parameter in Home Assistant scripts and automations.
|
||||||
|
|
||||||
|
## What Changed
|
||||||
|
|
||||||
|
- Added response schema support in the `ha_text_ai.ask_question` service
|
||||||
|
- Service is now correctly registered with `supports_response=True` flag
|
||||||
|
- You can now use `response_variable` to capture AI response in a variable
|
||||||
|
|
||||||
|
## Example Usage in Script
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
context_messages: 0
|
||||||
|
temperature: 0.7
|
||||||
|
max_tokens: 1000
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "What time is it?"
|
||||||
|
response_variable: ai_response
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Usage in Automation
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alias: "Get AI Response"
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: input_boolean.ask_ai
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "What's the current weather?"
|
||||||
|
temperature: 0.7
|
||||||
|
max_tokens: 500
|
||||||
|
response_variable: weather_response
|
||||||
|
|
||||||
|
- action: notify.persistent_notification
|
||||||
|
data:
|
||||||
|
title: "AI Response"
|
||||||
|
message: "{{ weather_response.response_text }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Fields in response_variable
|
||||||
|
|
||||||
|
When you use `response_variable`, you will receive an object with the following fields:
|
||||||
|
|
||||||
|
- `response_text` (string) - The AI response text
|
||||||
|
- `tokens_used` (integer) - Total number of tokens used
|
||||||
|
- `prompt_tokens` (integer) - Number of tokens in the prompt
|
||||||
|
- `completion_tokens` (integer) - Number of tokens in the completion
|
||||||
|
- `model_used` (string) - The AI model that was used for the response
|
||||||
|
- `instance` (string) - The instance name that was used
|
||||||
|
- `question` (string) - The original question that was asked
|
||||||
|
- `timestamp` (string) - ISO timestamp when the response was generated
|
||||||
|
- `success` (boolean) - Whether the request was successful
|
||||||
|
- `error` (string) - Error message if the request failed
|
||||||
|
|
||||||
|
## Example Using Response Fields
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
action:
|
||||||
|
- action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "Tell me a joke"
|
||||||
|
response_variable: joke_response
|
||||||
|
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ joke_response.success }}"
|
||||||
|
|
||||||
|
- action: input_text.set_value
|
||||||
|
target:
|
||||||
|
entity_id: input_text.last_ai_response
|
||||||
|
data:
|
||||||
|
value: "{{ joke_response.response_text }}"
|
||||||
|
|
||||||
|
- action: input_number.set_value
|
||||||
|
target:
|
||||||
|
entity_id: input_number.tokens_used
|
||||||
|
data:
|
||||||
|
value: "{{ joke_response.tokens_used }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
action:
|
||||||
|
- action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "Test question"
|
||||||
|
response_variable: ai_result
|
||||||
|
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ ai_result.success }}"
|
||||||
|
sequence:
|
||||||
|
- action: notify.mobile_app_phone
|
||||||
|
data:
|
||||||
|
title: "AI Response"
|
||||||
|
message: "{{ ai_result.response_text }}"
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ not ai_result.success }}"
|
||||||
|
sequence:
|
||||||
|
- action: notify.mobile_app_phone
|
||||||
|
data:
|
||||||
|
title: "AI Error"
|
||||||
|
message: "Error: {{ ai_result.error }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from Old Approach
|
||||||
|
|
||||||
|
**Old method (without response_variable):**
|
||||||
|
```yaml
|
||||||
|
# Ask question
|
||||||
|
- action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "Hello!"
|
||||||
|
|
||||||
|
# Wait and read response from sensor
|
||||||
|
- delay: 00:00:05
|
||||||
|
- action: notify.mobile_app_phone
|
||||||
|
data:
|
||||||
|
message: "{{ states('sensor.ha_text_ai_gemini') }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
**New method (with response_variable):**
|
||||||
|
```yaml
|
||||||
|
# Ask question and get response immediately
|
||||||
|
- action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "Hello!"
|
||||||
|
response_variable: greeting_response
|
||||||
|
|
||||||
|
- action: notify.mobile_app_phone
|
||||||
|
data:
|
||||||
|
message: "{{ greeting_response.response_text }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
The new approach is more reliable as it doesn't require waiting and reading from the sensor.
|
||||||
@@ -39,11 +39,17 @@ from .const import (
|
|||||||
CONF_CONTEXT_MESSAGES,
|
CONF_CONTEXT_MESSAGES,
|
||||||
API_PROVIDER_OPENAI,
|
API_PROVIDER_OPENAI,
|
||||||
API_PROVIDER_ANTHROPIC,
|
API_PROVIDER_ANTHROPIC,
|
||||||
|
API_PROVIDER_DEEPSEEK,
|
||||||
|
API_PROVIDER_GEMINI,
|
||||||
DEFAULT_MODEL,
|
DEFAULT_MODEL,
|
||||||
|
DEFAULT_DEEPSEEK_MODEL,
|
||||||
|
DEFAULT_GEMINI_MODEL,
|
||||||
DEFAULT_TEMPERATURE,
|
DEFAULT_TEMPERATURE,
|
||||||
DEFAULT_MAX_TOKENS,
|
DEFAULT_MAX_TOKENS,
|
||||||
DEFAULT_OPENAI_ENDPOINT,
|
DEFAULT_OPENAI_ENDPOINT,
|
||||||
DEFAULT_ANTHROPIC_ENDPOINT,
|
DEFAULT_ANTHROPIC_ENDPOINT,
|
||||||
|
DEFAULT_DEEPSEEK_ENDPOINT,
|
||||||
|
DEFAULT_GEMINI_ENDPOINT,
|
||||||
DEFAULT_REQUEST_INTERVAL,
|
DEFAULT_REQUEST_INTERVAL,
|
||||||
DEFAULT_CONTEXT_MESSAGES,
|
DEFAULT_CONTEXT_MESSAGES,
|
||||||
API_TIMEOUT,
|
API_TIMEOUT,
|
||||||
@@ -106,11 +112,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
# Initialize domain data storage
|
# Initialize domain data storage
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
async def async_ask_question(call: ServiceCall) -> None:
|
async def async_ask_question(call: ServiceCall) -> dict:
|
||||||
"""Handle ask_question service."""
|
"""Handle ask_question service with response data."""
|
||||||
try:
|
try:
|
||||||
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||||
await coordinator.async_ask_question(
|
response = await coordinator.async_ask_question(
|
||||||
question=call.data["question"],
|
question=call.data["question"],
|
||||||
model=call.data.get("model"),
|
model=call.data.get("model"),
|
||||||
temperature=call.data.get("temperature"),
|
temperature=call.data.get("temperature"),
|
||||||
@@ -118,9 +124,34 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
system_prompt=call.data.get("system_prompt"),
|
system_prompt=call.data.get("system_prompt"),
|
||||||
context_messages=call.data.get("context_messages"),
|
context_messages=call.data.get("context_messages"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Return structured response data
|
||||||
|
return {
|
||||||
|
"response_text": response.get("content", ""),
|
||||||
|
"tokens_used": response.get("tokens", {}).get("total", 0),
|
||||||
|
"prompt_tokens": response.get("tokens", {}).get("prompt", 0),
|
||||||
|
"completion_tokens": response.get("tokens", {}).get("completion", 0),
|
||||||
|
"model_used": response.get("model", call.data.get("model", coordinator.model)),
|
||||||
|
"instance": call.data["instance"],
|
||||||
|
"question": call.data["question"],
|
||||||
|
"timestamp": response.get("timestamp"),
|
||||||
|
"success": True
|
||||||
|
}
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.error("Error asking question: %s", str(err))
|
_LOGGER.error("Error asking question: %s", str(err))
|
||||||
raise HomeAssistantError(f"Failed to process question: {str(err)}")
|
# Return error response
|
||||||
|
return {
|
||||||
|
"response_text": "",
|
||||||
|
"tokens_used": 0,
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 0,
|
||||||
|
"model_used": call.data.get("model", ""),
|
||||||
|
"instance": call.data["instance"],
|
||||||
|
"question": call.data["question"],
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"success": False,
|
||||||
|
"error": str(err)
|
||||||
|
}
|
||||||
|
|
||||||
async def async_clear_history(call: ServiceCall) -> None:
|
async def async_clear_history(call: ServiceCall) -> None:
|
||||||
"""Handle clear_history service."""
|
"""Handle clear_history service."""
|
||||||
@@ -157,7 +188,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_ASK_QUESTION,
|
SERVICE_ASK_QUESTION,
|
||||||
async_ask_question,
|
async_ask_question,
|
||||||
schema=SERVICE_SCHEMA_ASK_QUESTION
|
schema=SERVICE_SCHEMA_ASK_QUESTION,
|
||||||
|
supports_response=True
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
@@ -233,8 +265,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
async def async_check_api(session, endpoint: str, headers: dict, provider: str) -> bool:
|
async def async_check_api(session, endpoint: str, headers: dict, provider: str) -> bool:
|
||||||
"""Check API availability for different providers."""
|
"""Check API availability for different providers."""
|
||||||
try:
|
try:
|
||||||
if provider == API_PROVIDER_ANTHROPIC:
|
if provider == API_PROVIDER_GEMINI:
|
||||||
|
# Gemini API does not support GET /models for validation, just check key presence
|
||||||
|
if headers.get("Authorization", "").replace("Bearer ", ""):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
_LOGGER.error("Gemini API key is missing or empty")
|
||||||
|
return False
|
||||||
|
elif provider == API_PROVIDER_ANTHROPIC:
|
||||||
check_url = f"{endpoint}/v1/models"
|
check_url = f"{endpoint}/v1/models"
|
||||||
|
elif provider == API_PROVIDER_DEEPSEEK:
|
||||||
|
check_url = f"{endpoint}/models"
|
||||||
else: # OpenAI
|
else: # OpenAI
|
||||||
check_url = f"{endpoint}/models"
|
check_url = f"{endpoint}/models"
|
||||||
|
|
||||||
@@ -243,7 +284,8 @@ async def async_check_api(session, endpoint: str, headers: dict, provider: str)
|
|||||||
if response.status in [200, 404]:
|
if response.status in [200, 404]:
|
||||||
return True
|
return True
|
||||||
elif response.status == 401:
|
elif response.status == 401:
|
||||||
raise ConfigEntryNotReady("Invalid API key")
|
_LOGGER.error("Invalid API key")
|
||||||
|
return False
|
||||||
elif response.status == 429:
|
elif response.status == 429:
|
||||||
_LOGGER.warning("Rate limit exceeded during API check")
|
_LOGGER.warning("Rate limit exceeded during API check")
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import asyncio
|
|||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
from aiohttp import ClientSession, ClientTimeout
|
from aiohttp import ClientSession, ClientTimeout
|
||||||
from async_timeout import timeout
|
from async_timeout import timeout
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
@@ -18,6 +19,9 @@ from .const import (
|
|||||||
API_TIMEOUT,
|
API_TIMEOUT,
|
||||||
API_RETRY_COUNT,
|
API_RETRY_COUNT,
|
||||||
API_PROVIDER_ANTHROPIC,
|
API_PROVIDER_ANTHROPIC,
|
||||||
|
API_PROVIDER_DEEPSEEK,
|
||||||
|
API_PROVIDER_OPENAI,
|
||||||
|
API_PROVIDER_GEMINI,
|
||||||
MIN_TEMPERATURE,
|
MIN_TEMPERATURE,
|
||||||
MAX_TEMPERATURE,
|
MAX_TEMPERATURE,
|
||||||
MIN_MAX_TOKENS,
|
MIN_MAX_TOKENS,
|
||||||
@@ -45,20 +49,36 @@ class APIClient:
|
|||||||
self.api_provider = api_provider
|
self.api_provider = api_provider
|
||||||
self.model = model
|
self.model = model
|
||||||
self.timeout = ClientTimeout(total=API_TIMEOUT)
|
self.timeout = ClientTimeout(total=API_TIMEOUT)
|
||||||
|
self._closed = False
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
"""Async context manager entry."""
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
"""Async context manager exit."""
|
||||||
|
await self.shutdown()
|
||||||
|
|
||||||
def _validate_parameters(
|
def _validate_parameters(
|
||||||
self,
|
self,
|
||||||
temperature: float,
|
temperature: float,
|
||||||
max_tokens: int,
|
max_tokens: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Validate API parameters."""
|
"""Validate API parameters with enhanced type checking."""
|
||||||
|
# Type validation
|
||||||
|
if not isinstance(temperature, (int, float)):
|
||||||
|
raise TypeError(f"Temperature must be a number, got {type(temperature)}")
|
||||||
|
if not isinstance(max_tokens, int):
|
||||||
|
raise TypeError(f"Max tokens must be an integer, got {type(max_tokens)}")
|
||||||
|
|
||||||
|
# Range validation
|
||||||
if not MIN_TEMPERATURE <= temperature <= MAX_TEMPERATURE:
|
if not MIN_TEMPERATURE <= temperature <= MAX_TEMPERATURE:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Temperature must be between {MIN_TEMPERATURE} and {MAX_TEMPERATURE}"
|
f"Temperature must be between {MIN_TEMPERATURE} and {MAX_TEMPERATURE}, got {temperature}"
|
||||||
)
|
)
|
||||||
if not MIN_MAX_TOKENS <= max_tokens <= MAX_MAX_TOKENS:
|
if not MIN_MAX_TOKENS <= max_tokens <= MAX_MAX_TOKENS:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Max tokens must be between {MIN_MAX_TOKENS} and {MAX_MAX_TOKENS}"
|
f"Max tokens must be between {MIN_MAX_TOKENS} and {MAX_MAX_TOKENS}, got {max_tokens}"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _make_request(
|
async def _make_request(
|
||||||
@@ -67,7 +87,10 @@ 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}")
|
# Log request without sensitive data
|
||||||
|
safe_payload = {k: v for k, v in payload.items() if k not in ['messages', 'system']}
|
||||||
|
_LOGGER.debug(f"API Request: URL={url}, Safe payload: {safe_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):
|
||||||
@@ -80,16 +103,18 @@ class APIClient:
|
|||||||
_LOGGER.debug(f"Response status: {response.status}")
|
_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}")
|
# Log error without sensitive data
|
||||||
raise HomeAssistantError(f"API error: {error_data}")
|
safe_error = {k: v for k, v in error_data.items() if k not in ['message', 'details']}
|
||||||
|
_LOGGER.error(f"API error (status {response.status}): {safe_error}")
|
||||||
|
raise HomeAssistantError(f"API error: status {response.status}")
|
||||||
return await response.json()
|
return await response.json()
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
_LOGGER.warning(f"Timeout on attempt {attempt + 1}")
|
_LOGGER.warning(f"Timeout on attempt {attempt + 1}/{API_RETRY_COUNT}")
|
||||||
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)}")
|
_LOGGER.warning(f"API request failed on attempt {attempt + 1}/{API_RETRY_COUNT}: {type(e).__name__}")
|
||||||
if attempt == API_RETRY_COUNT - 1:
|
if attempt == API_RETRY_COUNT - 1:
|
||||||
raise
|
raise
|
||||||
await asyncio.sleep(1 * (attempt + 1))
|
await asyncio.sleep(1 * (attempt + 1))
|
||||||
@@ -109,19 +134,53 @@ class APIClient:
|
|||||||
return await self._create_anthropic_completion(
|
return await self._create_anthropic_completion(
|
||||||
model, messages, temperature, max_tokens
|
model, messages, temperature, max_tokens
|
||||||
)
|
)
|
||||||
|
elif self.api_provider == API_PROVIDER_DEEPSEEK:
|
||||||
|
return await self._create_deepseek_completion(
|
||||||
|
model, messages, temperature, max_tokens
|
||||||
|
)
|
||||||
|
elif self.api_provider == API_PROVIDER_GEMINI:
|
||||||
|
return await self._create_gemini_completion(
|
||||||
|
model, messages, temperature, max_tokens
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
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)}")
|
||||||
|
|
||||||
|
async def _create_deepseek_completion(
|
||||||
|
self,
|
||||||
|
model: str,
|
||||||
|
messages: List[Dict[str, str]],
|
||||||
|
temperature: float,
|
||||||
|
max_tokens: int,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Create completion using DeepSeek API."""
|
||||||
|
url = f"{self.endpoint}/chat/completions"
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"messages": messages,
|
||||||
|
"temperature": temperature,
|
||||||
|
"max_tokens": max_tokens,
|
||||||
|
"stream": False
|
||||||
|
}
|
||||||
|
|
||||||
|
data = await self._make_request(url, payload)
|
||||||
|
return {
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"message": {"content": data["choices"][0]["message"]["content"]},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": data["usage"]["prompt_tokens"],
|
||||||
|
"completion_tokens": data["usage"]["completion_tokens"],
|
||||||
|
"total_tokens": data["usage"]["total_tokens"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
async def _create_openai_completion(
|
async def _create_openai_completion(
|
||||||
self,
|
self,
|
||||||
model: str,
|
model: str,
|
||||||
@@ -206,6 +265,143 @@ class APIClient:
|
|||||||
_LOGGER.error(f"Connection check failed: {str(e)}")
|
_LOGGER.error(f"Connection check failed: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def _create_gemini_completion(
|
||||||
|
self,
|
||||||
|
model: str,
|
||||||
|
messages: List[Dict[str, str]],
|
||||||
|
temperature: float,
|
||||||
|
max_tokens: int,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Create completion using Gemini API with google-genai library.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: The model name to use
|
||||||
|
messages: List of message dictionaries with role and content
|
||||||
|
temperature: Sampling temperature between 0.0 and 2.0
|
||||||
|
max_tokens: Maximum number of tokens to generate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with response content and token usage
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
def import_genai():
|
||||||
|
from google import genai
|
||||||
|
return genai
|
||||||
|
|
||||||
|
genai = await asyncio.to_thread(import_genai)
|
||||||
|
|
||||||
|
# Extract API key from headers (Bearer token)
|
||||||
|
api_key = self.headers.get("Authorization", "").replace("Bearer ", "")
|
||||||
|
|
||||||
|
def create_client():
|
||||||
|
if self.endpoint and self.endpoint != "https://generativelanguage.googleapis.com/v1beta":
|
||||||
|
return genai.Client(api_key=api_key, transport="rest",
|
||||||
|
client_options={"api_endpoint": self.endpoint})
|
||||||
|
else:
|
||||||
|
return genai.Client(api_key=api_key)
|
||||||
|
|
||||||
|
client = await asyncio.to_thread(create_client)
|
||||||
|
|
||||||
|
# Process messages to extract system instruction and chat history
|
||||||
|
system_instruction = ""
|
||||||
|
contents = []
|
||||||
|
|
||||||
|
for msg in messages:
|
||||||
|
if msg['role'] == 'system':
|
||||||
|
system_instruction += msg['content'] + "\n"
|
||||||
|
else:
|
||||||
|
# For chat history, we need to convert to the format Gemini expects
|
||||||
|
role = "user" if msg['role'] == 'user' else "model"
|
||||||
|
contents.append({
|
||||||
|
"role": role,
|
||||||
|
"parts": [{"text": msg['content']}]
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create configuration
|
||||||
|
def create_config():
|
||||||
|
from google.genai import types
|
||||||
|
config = types.GenerateContentConfig(
|
||||||
|
temperature=temperature,
|
||||||
|
max_output_tokens=max_tokens,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add system instruction if present
|
||||||
|
if system_instruction:
|
||||||
|
config.system_instruction = system_instruction.strip()
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
config = await asyncio.to_thread(create_config)
|
||||||
|
|
||||||
|
def generate_content():
|
||||||
|
# For single message without history, use generate_content
|
||||||
|
if len(contents) <= 1:
|
||||||
|
# If we have no content yet, create a simple prompt
|
||||||
|
if not contents:
|
||||||
|
prompt = "I need your assistance."
|
||||||
|
else:
|
||||||
|
prompt = contents[0]["parts"][0]["text"]
|
||||||
|
|
||||||
|
return client.models.generate_content(
|
||||||
|
model=model,
|
||||||
|
contents=prompt,
|
||||||
|
config=config
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# For multi-turn conversations, use chat
|
||||||
|
chat = client.chats.create(model=model, config=config)
|
||||||
|
|
||||||
|
# Send all messages in sequence
|
||||||
|
for content in contents:
|
||||||
|
if content["role"] == "user":
|
||||||
|
response = chat.send_message(content["parts"][0]["text"])
|
||||||
|
# We don't send assistant messages as they're already part of the history
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
response = await asyncio.to_thread(generate_content)
|
||||||
|
|
||||||
|
# Extract response text
|
||||||
|
def extract_response():
|
||||||
|
response_text = response.text if hasattr(response, 'text') else ""
|
||||||
|
|
||||||
|
# Try to get token usage if available
|
||||||
|
usage = {}
|
||||||
|
if hasattr(response, 'usage_metadata'):
|
||||||
|
usage = {
|
||||||
|
"prompt_tokens": getattr(response.usage_metadata, 'prompt_token_count', 0),
|
||||||
|
"completion_tokens": getattr(response.usage_metadata, 'candidates_token_count', 0),
|
||||||
|
"total_tokens": getattr(response.usage_metadata, 'total_token_count', 0)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Estimate token count as fallback
|
||||||
|
usage = {
|
||||||
|
"prompt_tokens": len(" ".join([m["content"] for m in messages]).split()) // 3,
|
||||||
|
"completion_tokens": len(response_text.split()) // 3,
|
||||||
|
"total_tokens": 0 # Will be calculated below
|
||||||
|
}
|
||||||
|
usage["total_tokens"] = usage["prompt_tokens"] + usage["completion_tokens"]
|
||||||
|
|
||||||
|
return response_text, usage
|
||||||
|
|
||||||
|
response_text, usage = await asyncio.to_thread(extract_response)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"choices": [{
|
||||||
|
"message": {
|
||||||
|
"content": response_text
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"usage": usage
|
||||||
|
}
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
_LOGGER.error(f"Google Gemini library not installed: {str(e)}")
|
||||||
|
raise HomeAssistantError(f"Missing dependency: {str(e)}. Please install google-genai.")
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(f"Gemini API error: {str(e)}")
|
||||||
|
raise HomeAssistantError(f"Gemini API error: {str(e)}")
|
||||||
|
|
||||||
async def shutdown(self) -> None:
|
async def shutdown(self) -> None:
|
||||||
"""Shutdown API client."""
|
"""Shutdown API client."""
|
||||||
_LOGGER.debug("Shutting down API client")
|
_LOGGER.debug("Shutting down API client")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Config flow for HA text AI integration.
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
@@ -28,13 +29,19 @@ from .const import (
|
|||||||
CONF_CONTEXT_MESSAGES,
|
CONF_CONTEXT_MESSAGES,
|
||||||
API_PROVIDER_OPENAI,
|
API_PROVIDER_OPENAI,
|
||||||
API_PROVIDER_ANTHROPIC,
|
API_PROVIDER_ANTHROPIC,
|
||||||
|
API_PROVIDER_DEEPSEEK,
|
||||||
|
API_PROVIDER_GEMINI,
|
||||||
API_PROVIDERS,
|
API_PROVIDERS,
|
||||||
DEFAULT_MODEL,
|
DEFAULT_MODEL,
|
||||||
|
DEFAULT_DEEPSEEK_MODEL,
|
||||||
|
DEFAULT_GEMINI_MODEL,
|
||||||
DEFAULT_TEMPERATURE,
|
DEFAULT_TEMPERATURE,
|
||||||
DEFAULT_MAX_TOKENS,
|
DEFAULT_MAX_TOKENS,
|
||||||
DEFAULT_REQUEST_INTERVAL,
|
DEFAULT_REQUEST_INTERVAL,
|
||||||
DEFAULT_OPENAI_ENDPOINT,
|
DEFAULT_OPENAI_ENDPOINT,
|
||||||
DEFAULT_ANTHROPIC_ENDPOINT,
|
DEFAULT_ANTHROPIC_ENDPOINT,
|
||||||
|
DEFAULT_DEEPSEEK_ENDPOINT,
|
||||||
|
DEFAULT_GEMINI_ENDPOINT,
|
||||||
DEFAULT_CONTEXT_MESSAGES,
|
DEFAULT_CONTEXT_MESSAGES,
|
||||||
MIN_TEMPERATURE,
|
MIN_TEMPERATURE,
|
||||||
MAX_TEMPERATURE,
|
MAX_TEMPERATURE,
|
||||||
@@ -90,9 +97,19 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self._errors = {}
|
self._errors = {}
|
||||||
|
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
default_endpoint = (
|
# Selecting an endpoint by provider
|
||||||
DEFAULT_OPENAI_ENDPOINT if self._provider == API_PROVIDER_OPENAI
|
default_endpoint = {
|
||||||
else DEFAULT_ANTHROPIC_ENDPOINT
|
API_PROVIDER_OPENAI: DEFAULT_OPENAI_ENDPOINT,
|
||||||
|
API_PROVIDER_ANTHROPIC: DEFAULT_ANTHROPIC_ENDPOINT,
|
||||||
|
API_PROVIDER_DEEPSEEK: DEFAULT_DEEPSEEK_ENDPOINT,
|
||||||
|
API_PROVIDER_GEMINI: DEFAULT_GEMINI_ENDPOINT,
|
||||||
|
}.get(self._provider, DEFAULT_OPENAI_ENDPOINT)
|
||||||
|
|
||||||
|
# Selecting the default model by provider
|
||||||
|
default_model = (
|
||||||
|
DEFAULT_DEEPSEEK_MODEL if self._provider == API_PROVIDER_DEEPSEEK else
|
||||||
|
DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else
|
||||||
|
DEFAULT_MODEL
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
@@ -100,7 +117,7 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
data_schema=vol.Schema({
|
data_schema=vol.Schema({
|
||||||
vol.Required(CONF_NAME, default="my_assistant"): 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,
|
||||||
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
||||||
vol.Coerce(float),
|
vol.Coerce(float),
|
||||||
@@ -131,43 +148,173 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Debug log to identify what's in the input
|
||||||
|
_LOGGER.debug(f"Provider step input data: {user_input}")
|
||||||
|
|
||||||
input_copy = user_input.copy()
|
input_copy = user_input.copy()
|
||||||
|
|
||||||
|
# Check if CONF_NAME exists in input_copy and ensure it's not empty
|
||||||
|
if CONF_NAME not in input_copy or not input_copy[CONF_NAME]:
|
||||||
|
_LOGGER.warning(f"Missing name in configuration input: {input_copy}")
|
||||||
|
input_copy[CONF_NAME] = f"gemini_assistant_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||||
|
_LOGGER.info(f"Auto-generated name: {input_copy[CONF_NAME]}")
|
||||||
|
|
||||||
|
# Ensure API key is present
|
||||||
|
if CONF_API_KEY not in input_copy or not input_copy[CONF_API_KEY]:
|
||||||
|
self._errors["base"] = "invalid_auth"
|
||||||
|
_LOGGER.error("API validation error: 'api_key'")
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="provider",
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
|
||||||
|
vol.Required(CONF_API_KEY): str,
|
||||||
|
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else DEFAULT_MODEL)): str,
|
||||||
|
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_GEMINI_ENDPOINT if self._provider == API_PROVIDER_GEMINI else DEFAULT_OPENAI_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)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_MAX_TOKENS, default=input_copy.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_REQUEST_INTERVAL, default=input_copy.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_REQUEST_INTERVAL)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
default=input_copy.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=20)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MAX_HISTORY_SIZE,
|
||||||
|
default=input_copy.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=100)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
errors=self._errors
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Validate and normalize the name
|
||||||
normalized_name = self._validate_and_normalize_name(input_copy[CONF_NAME])
|
normalized_name = self._validate_and_normalize_name(input_copy[CONF_NAME])
|
||||||
input_copy[CONF_NAME] = normalized_name
|
input_copy[CONF_NAME] = normalized_name
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
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=input_copy[CONF_NAME]): str,
|
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
|
||||||
vol.Required(CONF_API_KEY, default=input_copy[CONF_API_KEY]): str,
|
vol.Required(CONF_API_KEY, default=input_copy.get(CONF_API_KEY, "")): str,
|
||||||
vol.Required(CONF_MODEL, default=input_copy[CONF_MODEL]): str,
|
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else DEFAULT_MODEL)): str,
|
||||||
vol.Required(CONF_API_ENDPOINT, default=input_copy[CONF_API_ENDPOINT]): str,
|
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_GEMINI_ENDPOINT if self._provider == API_PROVIDER_GEMINI else DEFAULT_OPENAI_ENDPOINT)): str,
|
||||||
vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
|
vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
|
||||||
vol.Coerce(float),
|
vol.Coerce(float),
|
||||||
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||||
),
|
),
|
||||||
|
vol.Optional(CONF_MAX_TOKENS, default=input_copy.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_REQUEST_INTERVAL, default=input_copy.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_REQUEST_INTERVAL)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
default=input_copy.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=20)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MAX_HISTORY_SIZE,
|
||||||
|
default=input_copy.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=100)
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
errors={"name": str(e)}
|
errors={"name": str(e)}
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not await self._async_validate_api(input_copy):
|
# Special handling for Gemini API validation
|
||||||
return self.async_show_form(
|
if self._provider == API_PROVIDER_GEMINI:
|
||||||
step_id="provider",
|
# For Gemini, we just check if API key is present as there's no simple endpoint to validate
|
||||||
data_schema=vol.Schema({
|
if not input_copy.get(CONF_API_KEY):
|
||||||
}),
|
self._errors["base"] = "invalid_auth"
|
||||||
errors=self._errors
|
_LOGGER.error("API validation error: 'api_key'")
|
||||||
)
|
return self.async_show_form(
|
||||||
|
step_id="provider",
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
|
||||||
|
vol.Required(CONF_API_KEY): str,
|
||||||
|
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_GEMINI_MODEL)): str,
|
||||||
|
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_GEMINI_ENDPOINT)): str,
|
||||||
|
# Other fields remain the same
|
||||||
|
}),
|
||||||
|
errors=self._errors
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# For other providers, validate API connection
|
||||||
|
if not await self._async_validate_api(input_copy):
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="provider",
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
|
||||||
|
vol.Required(CONF_API_KEY, default=input_copy.get(CONF_API_KEY, "")): str,
|
||||||
|
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_MODEL)): str,
|
||||||
|
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_OPENAI_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)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_MAX_TOKENS, default=input_copy.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_REQUEST_INTERVAL, default=input_copy.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_REQUEST_INTERVAL)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
default=input_copy.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=20)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MAX_HISTORY_SIZE,
|
||||||
|
default=input_copy.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=100)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
errors=self._errors
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Handle any unexpected exceptions during validation
|
||||||
|
_LOGGER.exception("Unexpected error during API validation")
|
||||||
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=input_copy.get(CONF_NAME, "my_assistant")): str,
|
||||||
|
vol.Required(CONF_API_KEY, default=input_copy.get(CONF_API_KEY, "")): str,
|
||||||
|
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else DEFAULT_MODEL)): str,
|
||||||
|
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_GEMINI_ENDPOINT if self._provider == API_PROVIDER_GEMINI else DEFAULT_OPENAI_ENDPOINT)): str,
|
||||||
|
# Other fields remain the same
|
||||||
}),
|
}),
|
||||||
errors={"base": str(e)}
|
errors={"base": str(e)}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# All validation passed, create the entry
|
||||||
return await self._create_entry(input_copy)
|
return await self._create_entry(input_copy)
|
||||||
|
|
||||||
def _validate_and_normalize_name(self, name: str) -> str:
|
def _validate_and_normalize_name(self, name: str) -> str:
|
||||||
@@ -205,23 +352,34 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
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."""
|
||||||
try:
|
try:
|
||||||
|
if CONF_API_KEY not in user_input:
|
||||||
|
_LOGGER.error("API validation error: 'api_key'")
|
||||||
|
self._errors["base"] = "invalid_auth"
|
||||||
|
return False
|
||||||
|
|
||||||
session = async_get_clientsession(self.hass)
|
session = async_get_clientsession(self.hass)
|
||||||
headers = self._get_api_headers(user_input)
|
headers = self._get_api_headers(user_input)
|
||||||
endpoint = user_input[CONF_API_ENDPOINT].rstrip('/')
|
endpoint = user_input[CONF_API_ENDPOINT].rstrip('/')
|
||||||
|
|
||||||
check_url = (
|
if self._provider == API_PROVIDER_GEMINI:
|
||||||
f"{endpoint}/v1/models" if self._provider == API_PROVIDER_ANTHROPIC
|
if not user_input[CONF_API_KEY]:
|
||||||
else f"{endpoint}/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
async with session.get(check_url, headers=headers) as response:
|
|
||||||
if response.status == 401:
|
|
||||||
self._errors["base"] = "invalid_auth"
|
self._errors["base"] = "invalid_auth"
|
||||||
return False
|
return False
|
||||||
elif response.status not in [200, 404]:
|
|
||||||
self._errors["base"] = "cannot_connect"
|
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
else:
|
||||||
|
check_url = (
|
||||||
|
f"{endpoint}/v1/models" if self._provider == API_PROVIDER_ANTHROPIC
|
||||||
|
else f"{endpoint}/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
async with session.get(check_url, headers=headers) as response:
|
||||||
|
if response.status == 401:
|
||||||
|
self._errors["base"] = "invalid_auth"
|
||||||
|
return False
|
||||||
|
elif response.status not in [200, 404]:
|
||||||
|
self._errors["base"] = "cannot_connect"
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.error("API validation error: %s", str(err))
|
_LOGGER.error("API validation error: %s", str(err))
|
||||||
@@ -230,6 +388,9 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]:
|
def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]:
|
||||||
"""Get API headers based on provider."""
|
"""Get API headers based on provider."""
|
||||||
|
if CONF_API_KEY not in user_input:
|
||||||
|
return {"Content-Type": "application/json"}
|
||||||
|
|
||||||
api_key = user_input[CONF_API_KEY]
|
api_key = user_input[CONF_API_KEY]
|
||||||
|
|
||||||
if self._provider == API_PROVIDER_ANTHROPIC:
|
if self._provider == API_PROVIDER_ANTHROPIC:
|
||||||
@@ -238,6 +399,11 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
"anthropic-version": "2023-06-01",
|
"anthropic-version": "2023-06-01",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
|
elif self._provider == API_PROVIDER_GEMINI:
|
||||||
|
return {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
"Authorization": f"Bearer {api_key}",
|
"Authorization": f"Bearer {api_key}",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -250,6 +416,12 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
unique_id = f"{DOMAIN}_{normalized_name}_{self._provider}".lower()
|
unique_id = f"{DOMAIN}_{normalized_name}_{self._provider}".lower()
|
||||||
|
|
||||||
|
default_model = (
|
||||||
|
DEFAULT_DEEPSEEK_MODEL if self._provider == API_PROVIDER_DEEPSEEK else
|
||||||
|
DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else
|
||||||
|
DEFAULT_MODEL
|
||||||
|
)
|
||||||
|
|
||||||
entry_data = {
|
entry_data = {
|
||||||
CONF_API_PROVIDER: self._provider,
|
CONF_API_PROVIDER: self._provider,
|
||||||
CONF_NAME: instance_name,
|
CONF_NAME: instance_name,
|
||||||
@@ -257,7 +429,7 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
CONF_API_KEY: user_input.get(CONF_API_KEY),
|
CONF_API_KEY: user_input.get(CONF_API_KEY),
|
||||||
CONF_API_ENDPOINT: user_input.get(CONF_API_ENDPOINT),
|
CONF_API_ENDPOINT: user_input.get(CONF_API_ENDPOINT),
|
||||||
"unique_id": unique_id,
|
"unique_id": unique_id,
|
||||||
CONF_MODEL: user_input.get(CONF_MODEL, DEFAULT_MODEL),
|
CONF_MODEL: user_input.get(CONF_MODEL, default_model),
|
||||||
CONF_TEMPERATURE: user_input.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
|
CONF_TEMPERATURE: user_input.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
|
||||||
CONF_MAX_TOKENS: user_input.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
|
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_REQUEST_INTERVAL: user_input.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
|
||||||
@@ -296,13 +468,20 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
return self.async_create_entry(title="", data=user_input)
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
current_data = {**self.config_entry.data, **self.config_entry.options}
|
current_data = {**self.config_entry.data, **self.config_entry.options}
|
||||||
|
provider = current_data.get(CONF_API_PROVIDER)
|
||||||
|
|
||||||
|
default_model = (
|
||||||
|
DEFAULT_DEEPSEEK_MODEL if provider == API_PROVIDER_DEEPSEEK else
|
||||||
|
DEFAULT_GEMINI_MODEL if provider == API_PROVIDER_GEMINI else
|
||||||
|
DEFAULT_MODEL
|
||||||
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="init",
|
step_id="init",
|
||||||
data_schema=vol.Schema({
|
data_schema=vol.Schema({
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_MODEL,
|
CONF_MODEL,
|
||||||
default=current_data.get(CONF_MODEL, DEFAULT_MODEL)
|
default=current_data.get(CONF_MODEL, default_model)
|
||||||
): str,
|
): str,
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_TEMPERATURE,
|
CONF_TEMPERATURE,
|
||||||
|
|||||||
@@ -6,10 +6,14 @@ Constants for the HA text AI integration.
|
|||||||
@github: https://github.com/smkrv/ha-text-ai
|
@github: https://github.com/smkrv/ha-text-ai
|
||||||
@source: https://github.com/smkrv/ha-text-ai
|
@source: https://github.com/smkrv/ha-text-ai
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
from typing import Final
|
from typing import Final
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME
|
from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
import logging
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Domain and platforms
|
# Domain and platforms
|
||||||
DOMAIN: Final = "ha_text_ai"
|
DOMAIN: Final = "ha_text_ai"
|
||||||
@@ -19,15 +23,37 @@ PLATFORMS: list[str] = ["sensor"]
|
|||||||
CONF_API_PROVIDER: Final = "api_provider"
|
CONF_API_PROVIDER: Final = "api_provider"
|
||||||
API_PROVIDER_OPENAI: Final = "openai"
|
API_PROVIDER_OPENAI: Final = "openai"
|
||||||
API_PROVIDER_ANTHROPIC: Final = "anthropic"
|
API_PROVIDER_ANTHROPIC: Final = "anthropic"
|
||||||
|
API_PROVIDER_DEEPSEEK: Final = "deepseek"
|
||||||
|
API_PROVIDER_GEMINI: Final = "gemini"
|
||||||
|
|
||||||
API_PROVIDERS: Final = [
|
API_PROVIDERS: Final = [
|
||||||
API_PROVIDER_OPENAI,
|
API_PROVIDER_OPENAI,
|
||||||
API_PROVIDER_ANTHROPIC
|
API_PROVIDER_ANTHROPIC,
|
||||||
|
API_PROVIDER_DEEPSEEK,
|
||||||
|
API_PROVIDER_GEMINI
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Read version from manifest.json
|
||||||
|
MANIFEST_PATH = os.path.join(os.path.dirname(__file__), "manifest.json")
|
||||||
|
try:
|
||||||
|
with open(MANIFEST_PATH) as manifest_file:
|
||||||
|
manifest = json.load(manifest_file)
|
||||||
|
VERSION = manifest.get("version", "unknown")
|
||||||
|
except FileNotFoundError:
|
||||||
|
VERSION = "unknown"
|
||||||
|
_LOGGER.warning("manifest.json not found at %s", MANIFEST_PATH)
|
||||||
|
except json.JSONDecodeError as err:
|
||||||
|
VERSION = "unknown"
|
||||||
|
_LOGGER.error("Error decoding JSON from manifest.json: %s", err)
|
||||||
|
except Exception as err:
|
||||||
|
VERSION = "unknown"
|
||||||
|
_LOGGER.error("Error reading manifest.json: %s", err)
|
||||||
|
|
||||||
# Default endpoints
|
# Default endpoints
|
||||||
DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1"
|
DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1"
|
||||||
DEFAULT_ANTHROPIC_ENDPOINT: Final = "https://api.anthropic.com"
|
DEFAULT_ANTHROPIC_ENDPOINT: Final = "https://api.anthropic.com"
|
||||||
|
DEFAULT_DEEPSEEK_ENDPOINT: Final = "https://api.deepseek.com"
|
||||||
|
DEFAULT_GEMINI_ENDPOINT: Final = "https://generativelanguage.googleapis.com/v1beta"
|
||||||
|
|
||||||
# Configuration constants
|
# Configuration constants
|
||||||
CONF_MODEL: Final = "model"
|
CONF_MODEL: Final = "model"
|
||||||
@@ -47,6 +73,8 @@ ICONS_SUBDOMAIN = "icons"
|
|||||||
|
|
||||||
# Default values
|
# Default values
|
||||||
DEFAULT_MODEL: Final = "gpt-4o-mini"
|
DEFAULT_MODEL: Final = "gpt-4o-mini"
|
||||||
|
DEFAULT_DEEPSEEK_MODEL: Final = "deepseek-chat"
|
||||||
|
DEFAULT_GEMINI_MODEL: Final = "gemini-2.0-flash"
|
||||||
DEFAULT_TEMPERATURE: Final = 0.1
|
DEFAULT_TEMPERATURE: Final = 0.1
|
||||||
DEFAULT_MAX_TOKENS: Final = 1000
|
DEFAULT_MAX_TOKENS: Final = 1000
|
||||||
DEFAULT_REQUEST_INTERVAL: Final = 1.0
|
DEFAULT_REQUEST_INTERVAL: Final = 1.0
|
||||||
@@ -56,11 +84,13 @@ DEFAULT_NAME: Final = "HA Text AI"
|
|||||||
DEFAULT_NAME_PREFIX = "ha_text_ai"
|
DEFAULT_NAME_PREFIX = "ha_text_ai"
|
||||||
DEFAULT_CONTEXT_MESSAGES: Final = 5
|
DEFAULT_CONTEXT_MESSAGES: Final = 5
|
||||||
|
|
||||||
|
TRUNCATION_INDICATOR = " ... "
|
||||||
|
|
||||||
# Parameter constraints
|
# Parameter constraints
|
||||||
MIN_TEMPERATURE: Final = 0.0
|
MIN_TEMPERATURE: Final = 0.0
|
||||||
MAX_TEMPERATURE: Final = 2.0
|
MAX_TEMPERATURE: Final = 2.0
|
||||||
MIN_MAX_TOKENS: Final = 1
|
MIN_MAX_TOKENS: Final = 1
|
||||||
MAX_MAX_TOKENS: Final = 4096
|
MAX_MAX_TOKENS: Final = 100000
|
||||||
MIN_REQUEST_INTERVAL: Final = 0.1
|
MIN_REQUEST_INTERVAL: Final = 0.1
|
||||||
MAX_REQUEST_INTERVAL: Final = 60.0
|
MAX_REQUEST_INTERVAL: Final = 60.0
|
||||||
|
|
||||||
|
|||||||
@@ -41,23 +41,11 @@ from .const import (
|
|||||||
ABSOLUTE_MAX_HISTORY_SIZE,
|
ABSOLUTE_MAX_HISTORY_SIZE,
|
||||||
MAX_ATTRIBUTE_SIZE,
|
MAX_ATTRIBUTE_SIZE,
|
||||||
MAX_HISTORY_FILE_SIZE,
|
MAX_HISTORY_FILE_SIZE,
|
||||||
|
TRUNCATION_INDICATOR,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
def _check_memory_available(self):
|
|
||||||
"""Check if enough memory is available."""
|
|
||||||
memory = psutil.virtual_memory()
|
|
||||||
|
|
||||||
# Log the total and available memory
|
|
||||||
_LOGGER.debug("Total memory: %s, Available memory: %s", memory.total, memory.available)
|
|
||||||
|
|
||||||
if memory.available > 1024 * 1024 * 100: # 100MB
|
|
||||||
_LOGGER.debug("Sufficient memory available: %s bytes", memory.available)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
_LOGGER.warning("Insufficient memory available: %s bytes", memory.available)
|
|
||||||
return False
|
|
||||||
|
|
||||||
class AsyncFileHandler:
|
class AsyncFileHandler:
|
||||||
"""Async context manager for file operations."""
|
"""Async context manager for file operations."""
|
||||||
@@ -548,25 +536,57 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
def _sync_write_history_entry(self, entry: dict) -> None:
|
def _sync_write_history_entry(self, entry: dict) -> None:
|
||||||
"""Synchronous method to write history entry."""
|
"""Synchronous method to write history entry with enhanced error handling."""
|
||||||
try:
|
try:
|
||||||
history = []
|
history = []
|
||||||
if os.path.exists(self._history_file):
|
if os.path.exists(self._history_file):
|
||||||
with open(self._history_file, 'r') as f:
|
try:
|
||||||
content = f.read()
|
with open(self._history_file, 'r', encoding='utf-8') as f:
|
||||||
if content:
|
content = f.read()
|
||||||
history = json.loads(content)
|
if content.strip():
|
||||||
|
history = json.loads(content)
|
||||||
|
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
||||||
|
_LOGGER.warning(f"Corrupted history file, creating new: {e}")
|
||||||
|
# Backup corrupted file
|
||||||
|
backup_path = f"{self._history_file}.corrupted.{dt_util.utcnow().strftime('%Y%m%d_%H%M%S')}"
|
||||||
|
try:
|
||||||
|
os.rename(self._history_file, backup_path)
|
||||||
|
_LOGGER.info(f"Corrupted history backed up to: {backup_path}")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
except PermissionError as e:
|
||||||
|
_LOGGER.error(f"Permission denied reading history file: {e}")
|
||||||
|
raise
|
||||||
|
except OSError as e:
|
||||||
|
_LOGGER.error(f"OS error reading history file: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
history.append(entry)
|
history.append(entry)
|
||||||
|
|
||||||
if len(history) > self.max_history_size:
|
if len(history) > self.max_history_size:
|
||||||
history = history[-self.max_history_size:]
|
history = history[-self.max_history_size:]
|
||||||
|
|
||||||
with open(self._history_file, 'w') as f:
|
# Write with atomic operation
|
||||||
json.dump(history, f, indent=2)
|
temp_file = f"{self._history_file}.tmp"
|
||||||
|
try:
|
||||||
|
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(history, f, indent=2, ensure_ascii=False)
|
||||||
|
os.replace(temp_file, self._history_file)
|
||||||
|
except PermissionError as e:
|
||||||
|
_LOGGER.error(f"Permission denied writing history file: {e}")
|
||||||
|
raise
|
||||||
|
except OSError as e:
|
||||||
|
_LOGGER.error(f"OS error writing history file: {e}")
|
||||||
|
# Clean up temp file if it exists
|
||||||
|
try:
|
||||||
|
os.remove(temp_file)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_LOGGER.error(f"Synchronous history entry writing failed: {e}")
|
_LOGGER.error(f"Synchronous history entry writing failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
async def _rotate_history(self) -> None:
|
async def _rotate_history(self) -> None:
|
||||||
"""Rotate conversation history with file management."""
|
"""Rotate conversation history with file management."""
|
||||||
@@ -647,15 +667,14 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
|
|
||||||
async def _async_update_data(self) -> Dict[str, Any]:
|
async def _async_update_data(self) -> Dict[str, Any]:
|
||||||
"""Update coordinator data with improved error handling and performance."""
|
"""Update coordinator data with improved error handling and performance."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with asyncio.Semaphore(1):
|
async with asyncio.Semaphore(1):
|
||||||
current_state = self._get_current_state()
|
current_state = self._get_current_state()
|
||||||
|
|
||||||
limited_history = await self._get_limited_history()
|
# Get limited history with info
|
||||||
|
history_data = await self._get_limited_history()
|
||||||
|
|
||||||
metrics = await self._get_current_metrics()
|
metrics = await self._get_current_metrics()
|
||||||
|
|
||||||
if metrics is None:
|
if metrics is None:
|
||||||
metrics = {}
|
metrics = {}
|
||||||
|
|
||||||
@@ -670,47 +689,68 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
"uptime": self._calculate_uptime(),
|
"uptime": self._calculate_uptime(),
|
||||||
"system_prompt": self._get_truncated_system_prompt(),
|
"system_prompt": self._get_truncated_system_prompt(),
|
||||||
"history_size": len(self._conversation_history),
|
"history_size": len(self._conversation_history),
|
||||||
"conversation_history": limited_history,
|
"conversation_history": history_data["entries"],
|
||||||
|
"history_info": history_data["info"],
|
||||||
"normalized_name": self.normalized_name,
|
"normalized_name": self.normalized_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
await self._validate_update_data(data)
|
await self._validate_update_data(data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
_LOGGER.warning("Update was cancelled")
|
|
||||||
raise
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.error(f"Error updating data: {err}", exc_info=True)
|
_LOGGER.error(f"Error updating data: {err}", exc_info=True)
|
||||||
return self._get_safe_initial_state()
|
return self._get_safe_initial_state()
|
||||||
|
|
||||||
async def _get_limited_history(self) -> List[Dict[str, Any]]:
|
async def _get_limited_history(self) -> Dict[str, Any]:
|
||||||
"""Get limited conversation history with size constraints."""
|
"""Get limited conversation history showing only last Q&A."""
|
||||||
limited_history = []
|
limited_history = []
|
||||||
|
|
||||||
if self._conversation_history:
|
if self._conversation_history:
|
||||||
for entry in self._conversation_history[-3:]:
|
last_entry = self._conversation_history[-1]
|
||||||
limited_entry = {
|
limited_entry = {
|
||||||
"timestamp": entry["timestamp"],
|
"timestamp": last_entry["timestamp"],
|
||||||
"question": self._truncate_text(entry["question"]),
|
"question": last_entry["question"][:4096] + (TRUNCATION_INDICATOR if len(last_entry["question"]) > 4096 else ""),
|
||||||
"response": self._truncate_text(entry["response"]),
|
"response": last_entry["response"][:4096] + (TRUNCATION_INDICATOR if len(last_entry["response"]) > 4096 else ""),
|
||||||
}
|
}
|
||||||
limited_history.append(limited_entry)
|
limited_history.append(limited_entry)
|
||||||
return limited_history
|
|
||||||
|
history_info = {
|
||||||
|
"total_entries": len(self._conversation_history),
|
||||||
|
"displayed_entries": len(limited_history),
|
||||||
|
"full_history_available": True,
|
||||||
|
"history_path": self._history_file,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"entries": limited_history,
|
||||||
|
"full_history": self._conversation_history,
|
||||||
|
"info": history_info
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _get_sanitized_last_response(self) -> Dict[str, Any]:
|
||||||
|
"""Get sanitized version of last response with truncation indicators."""
|
||||||
|
response = self.last_response.copy()
|
||||||
|
|
||||||
|
if "response" in response:
|
||||||
|
original_response = response["response"]
|
||||||
|
is_response_truncated = len(original_response) > 4096
|
||||||
|
response["response"] = original_response[:4096] + (TRUNCATION_INDICATOR if is_response_truncated else "")
|
||||||
|
response["is_response_truncated"] = is_response_truncated
|
||||||
|
response["full_response_length"] = len(original_response)
|
||||||
|
|
||||||
|
if "question" in response:
|
||||||
|
original_question = response["question"]
|
||||||
|
is_question_truncated = len(original_question) > 4096
|
||||||
|
response["question"] = original_question[:4096] + (TRUNCATION_INDICATOR if is_question_truncated else "")
|
||||||
|
response["is_question_truncated"] = is_question_truncated
|
||||||
|
response["full_question_length"] = len(original_question)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
def _truncate_text(self, text: str, max_length: int = MAX_ATTRIBUTE_SIZE) -> str:
|
def _truncate_text(self, text: str, max_length: int = MAX_ATTRIBUTE_SIZE) -> str:
|
||||||
"""Safely truncate text to maximum length."""
|
"""Safely truncate text to maximum length."""
|
||||||
return text[:max_length] if text else ""
|
return text[:max_length] if text else ""
|
||||||
|
|
||||||
async def _get_sanitized_last_response(self) -> Dict[str, Any]:
|
|
||||||
"""Get sanitized version of last response."""
|
|
||||||
response = self.last_response.copy()
|
|
||||||
if "response" in response:
|
|
||||||
response["response"] = self._truncate_text(response["response"])
|
|
||||||
if "question" in response:
|
|
||||||
response["question"] = self._truncate_text(response["question"])
|
|
||||||
return response
|
|
||||||
|
|
||||||
def _calculate_uptime(self) -> float:
|
def _calculate_uptime(self) -> float:
|
||||||
"""Calculate current uptime in seconds."""
|
"""Calculate current uptime in seconds."""
|
||||||
return (dt_util.utcnow() - self._start_time).total_seconds()
|
return (dt_util.utcnow() - self._start_time).total_seconds()
|
||||||
@@ -775,46 +815,6 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
"normalized_name": self.normalized_name,
|
"normalized_name": self.normalized_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _calculate_context_tokens(self, messages: List[Dict[str, str]], model: Optional[str] = None) -> int:
|
|
||||||
total_tokens = 0
|
|
||||||
|
|
||||||
# Compile regular expressions for performance
|
|
||||||
number_pattern = re.compile(r'[0-9]')
|
|
||||||
special_char_pattern = re.compile(r'[^\w\s]')
|
|
||||||
whitespace_pattern = re.compile(r'\s+')
|
|
||||||
|
|
||||||
try:
|
|
||||||
for message in messages:
|
|
||||||
text = message.get('content', '')
|
|
||||||
if text:
|
|
||||||
# Normalize whitespace
|
|
||||||
text = whitespace_pattern.sub(' ', text.strip())
|
|
||||||
|
|
||||||
# Advanced token estimation heuristics
|
|
||||||
words = text.split()
|
|
||||||
for word in words:
|
|
||||||
# Complexity-based token calculation
|
|
||||||
if len(word) > 8: # Long words
|
|
||||||
total_tokens += 2
|
|
||||||
elif number_pattern.search(word): # Words with numbers
|
|
||||||
total_tokens += 1.5
|
|
||||||
elif special_char_pattern.search(word): # Words with special characters
|
|
||||||
total_tokens += 1.5
|
|
||||||
elif word.isupper(): # Acronyms and technical terms
|
|
||||||
total_tokens += 1.5
|
|
||||||
else: # Regular words
|
|
||||||
total_tokens += 1
|
|
||||||
|
|
||||||
# Additional correction for technical texts
|
|
||||||
total_tokens = math.ceil(total_tokens * 1.2)
|
|
||||||
|
|
||||||
return int(total_tokens)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
_LOGGER.error(f"Token calculation error: {e}. Processed {len(messages)} messages.")
|
|
||||||
# Safe fallback with minimal token estimation
|
|
||||||
return len(messages) * 100
|
|
||||||
|
|
||||||
async def async_ask_question(
|
async def async_ask_question(
|
||||||
self,
|
self,
|
||||||
question: str,
|
question: str,
|
||||||
@@ -855,9 +855,7 @@ 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 question with context management."""
|
||||||
Enhanced question processing with intelligent token management.
|
|
||||||
"""
|
|
||||||
if self.client is None:
|
if self.client is None:
|
||||||
raise HomeAssistantError("AI client not initialized")
|
raise HomeAssistantError("AI client not initialized")
|
||||||
|
|
||||||
@@ -879,62 +877,23 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
if temp_system_prompt:
|
if temp_system_prompt:
|
||||||
messages.append({"role": "system", "content": temp_system_prompt})
|
messages.append({"role": "system", "content": temp_system_prompt})
|
||||||
|
|
||||||
# Context history management
|
# Add context from history
|
||||||
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"]})
|
||||||
|
|
||||||
|
# Add current question
|
||||||
messages.append({"role": "user", "content": question})
|
messages.append({"role": "user", "content": question})
|
||||||
|
|
||||||
# Detailed token logging
|
# Process message
|
||||||
_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": min(temp_max_tokens, available_tokens),
|
"max_tokens": temp_max_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
|
||||||
@@ -963,23 +922,38 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
else:
|
else:
|
||||||
response = await self._process_openai_message(question, **kwargs)
|
response = await self._process_openai_message(question, **kwargs)
|
||||||
|
|
||||||
|
# Add timestamp and model information to response
|
||||||
|
timestamp = dt_util.utcnow().isoformat()
|
||||||
|
model_used = kwargs.get("model", self.model)
|
||||||
|
|
||||||
|
# Enhance response with metadata
|
||||||
|
enhanced_response = {
|
||||||
|
"content": response["content"],
|
||||||
|
"tokens": response.get("tokens", {}),
|
||||||
|
"model": model_used,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"instance": self.instance_name,
|
||||||
|
"question": question,
|
||||||
|
"success": True
|
||||||
|
}
|
||||||
|
|
||||||
self.last_response = {
|
self.last_response = {
|
||||||
"timestamp": dt_util.utcnow().isoformat(),
|
"timestamp": timestamp,
|
||||||
"question": question,
|
"question": question,
|
||||||
"response": response["content"],
|
"response": response["content"],
|
||||||
"model": kwargs.get("model", self.model),
|
"model": model_used,
|
||||||
"instance": self.instance_name,
|
"instance": self.instance_name,
|
||||||
"normalized_name": self.normalized_name,
|
"normalized_name": self.normalized_name,
|
||||||
"error": None,
|
"error": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return enhanced_response
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
raise HomeAssistantError("Request timed out")
|
raise HomeAssistantError("Request timed out")
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
self._handle_error(err)
|
await self._handle_error(err)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def _process_anthropic_message(self, question: str, **kwargs) -> dict:
|
async def _process_anthropic_message(self, question: str, **kwargs) -> dict:
|
||||||
@@ -1120,6 +1094,24 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
self._system_prompt = prompt
|
self._system_prompt = prompt
|
||||||
await self.async_update_ha_state()
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
|
def _check_memory_available(self) -> bool:
|
||||||
|
"""Check if enough memory is available."""
|
||||||
|
try:
|
||||||
|
memory = psutil.virtual_memory()
|
||||||
|
|
||||||
|
# Log the total and available memory
|
||||||
|
_LOGGER.debug("Total memory: %s, Available memory: %s", memory.total, memory.available)
|
||||||
|
|
||||||
|
if memory.available > 1024 * 1024 * 100: # 100MB
|
||||||
|
_LOGGER.debug("Sufficient memory available: %s bytes", memory.available)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("Insufficient memory available: %s bytes", memory.available)
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error("Error checking memory availability: %s", e)
|
||||||
|
return True # Assume memory is available if check fails
|
||||||
|
|
||||||
async def async_shutdown(self) -> None:
|
async def async_shutdown(self) -> None:
|
||||||
"""Shutdown coordinator."""
|
"""Shutdown coordinator."""
|
||||||
_LOGGER.debug(f"Shutting down coordinator for {self.instance_name}")
|
_LOGGER.debug(f"Shutting down coordinator for {self.instance_name}")
|
||||||
|
|||||||
@@ -13,16 +13,17 @@
|
|||||||
"loggers": ["custom_components.ha_text_ai"],
|
"loggers": ["custom_components.ha_text_ai"],
|
||||||
"mqtt": [],
|
"mqtt": [],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"openai>=1.12.0",
|
"openai>=1.12.0",
|
||||||
"anthropic>=0.8.0",
|
"anthropic>=0.8.0",
|
||||||
"aiohttp>=3.8.0",
|
"google-genai>=1.16.0",
|
||||||
"async-timeout>=4.0.0",
|
"aiohttp>=3.8.0",
|
||||||
"certifi>=2024.2.2"
|
"async-timeout>=4.0.0",
|
||||||
],
|
"certifi>=2024.2.2"
|
||||||
|
],
|
||||||
"single_config_entry": false,
|
"single_config_entry": false,
|
||||||
"ssdp": [],
|
"ssdp": [],
|
||||||
"usb": [],
|
"usb": [],
|
||||||
"version": "2.0.8",
|
"version": "2.1.8",
|
||||||
"zeroconf": []
|
"zeroconf": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Sensor platform for HA Text AI.
|
|||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
@@ -68,6 +69,7 @@ from .const import (
|
|||||||
DEFAULT_NAME_PREFIX,
|
DEFAULT_NAME_PREFIX,
|
||||||
CONF_MAX_HISTORY_SIZE,
|
CONF_MAX_HISTORY_SIZE,
|
||||||
MAX_ATTRIBUTE_SIZE,
|
MAX_ATTRIBUTE_SIZE,
|
||||||
|
VERSION,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .coordinator import HATextAICoordinator
|
from .coordinator import HATextAICoordinator
|
||||||
@@ -154,7 +156,7 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
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=VERSION,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ ask_question:
|
|||||||
description: >-
|
description: >-
|
||||||
Send a question to the AI model and receive a detailed response.
|
Send a question to the AI model and receive a detailed response.
|
||||||
The response will be stored in the conversation history and can be retrieved later.
|
The response will be stored in the conversation history and can be retrieved later.
|
||||||
|
This service now returns response data directly, eliminating the need to read from sensors.
|
||||||
fields:
|
fields:
|
||||||
instance:
|
instance:
|
||||||
name: Instance
|
name: Instance
|
||||||
@@ -63,13 +64,13 @@ ask_question:
|
|||||||
|
|
||||||
max_tokens:
|
max_tokens:
|
||||||
name: Max Tokens
|
name: Max Tokens
|
||||||
description: Maximum length of the response (1-4096 tokens)
|
description: Maximum length of the response (tokens)
|
||||||
required: false
|
required: false
|
||||||
default: 1000
|
default: 1000
|
||||||
selector:
|
selector:
|
||||||
number:
|
number:
|
||||||
min: 1
|
min: 1
|
||||||
max: 4096
|
max: 100000
|
||||||
step: 1
|
step: 1
|
||||||
mode: box
|
mode: box
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"model": "Zu verwendendes AI-Modell",
|
"model": "Zu verwendendes AI-Modell",
|
||||||
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
|
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
|
||||||
"temperature": "Kreativität der Antwort (0-2, niedriger = fokussierter)",
|
"temperature": "Kreativität der Antwort (0-2, niedriger = fokussierter)",
|
||||||
"max_tokens": "Maximale Länge der Antwort (1-4096 Token)",
|
"max_tokens": "Maximale Länge der Antwort (1-100000 Token)",
|
||||||
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)",
|
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)",
|
||||||
"context_messages": "Anzahl der zu behaltenden Kontextnachrichten (1-20)",
|
"context_messages": "Anzahl der zu behaltenden Kontextnachrichten (1-20)",
|
||||||
"max_history_size": "Maximale Größe des Gesprächsverlaufs (1-100)"
|
"max_history_size": "Maximale Größe des Gesprächsverlaufs (1-100)"
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"api_key": "API-Schlüssel zur Authentifizierung",
|
"api_key": "API-Schlüssel zur Authentifizierung",
|
||||||
"model": "Zu verwendendes AI-Modell",
|
"model": "Zu verwendendes AI-Modell",
|
||||||
"temperature": "Kreativität der Antwort (0-2, niedriger = fokussierter)",
|
"temperature": "Kreativität der Antwort (0-2, niedriger = fokussierter)",
|
||||||
"max_tokens": "Maximale Länge der Antwort (1-4096 Token)",
|
"max_tokens": "Maximale Länge der Antwort (1-100000 Token)",
|
||||||
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
|
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
|
||||||
"api_provider": "API-Anbieter",
|
"api_provider": "API-Anbieter",
|
||||||
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)",
|
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"model": "AI-Modell",
|
"model": "AI-Modell",
|
||||||
"temperature": "Kreativität der Antwort (0-2)",
|
"temperature": "Kreativität der Antwort (0-2)",
|
||||||
"max_tokens": "Maximale Länge der Antwort (1-4096)",
|
"max_tokens": "Maximale Länge der Antwort (1-100000)",
|
||||||
"request_interval": "Minimale Anfrageintervall (0,1-60 Sekunden)",
|
"request_interval": "Minimale Anfrageintervall (0,1-60 Sekunden)",
|
||||||
"context_messages": "Anzahl der vorherigen Nachrichten, die im Kontext enthalten sein sollen (1-20)",
|
"context_messages": "Anzahl der vorherigen Nachrichten, die im Kontext enthalten sein sollen (1-20)",
|
||||||
"max_history_size": "Maximale Größe des Gesprächsverlaufs (1-100)"
|
"max_history_size": "Maximale Größe des Gesprächsverlaufs (1-100)"
|
||||||
@@ -88,15 +88,17 @@
|
|||||||
"selector": {
|
"selector": {
|
||||||
"api_provider": {
|
"api_provider": {
|
||||||
"options": {
|
"options": {
|
||||||
"openai": "OpenAI (kompatibel)",
|
"openai": "OpenAI (compatible)",
|
||||||
"anthropic": "Anthropic (kompatibel)"
|
"anthropic": "Anthropic (compatible)",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"gemini": "Google Gemini"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Frage stellen (HA Text AI)",
|
"name": "Frage stellen (HA Text AI)",
|
||||||
"description": "Stellen Sie eine Frage an das AI-Modell und erhalten Sie eine detaillierte Antwort. Die Antwort wird im Gesprächsverlauf gespeichert und kann später abgerufen werden.",
|
"description": "Stellen Sie eine Frage an das AI-Modell und erhalten Sie eine detaillierte Antwort. Dieser Service gibt jetzt Antwortdaten direkt zurück, wodurch separate Textsensoren und die 255-Zeichen-Begrenzung überflüssig werden. Die Antwort wird auch im Gesprächsverlauf gespeichert.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Instanz",
|
"name": "Instanz",
|
||||||
@@ -124,7 +126,7 @@
|
|||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Max Tokens",
|
"name": "Max Tokens",
|
||||||
"description": "Maximale Länge der Antwort (1-4096 Token)"
|
"description": "Maximale Länge der Antwort (1-100000 Token)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"model": "AI model to use",
|
"model": "AI model to use",
|
||||||
"api_endpoint": "Custom API endpoint URL (optional)",
|
"api_endpoint": "Custom API endpoint URL (optional)",
|
||||||
"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-100000 tokens)",
|
||||||
"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)"
|
"max_history_size": "Maximum conversation history size (1-100)"
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"api_key": "API key for authentication",
|
"api_key": "API key for authentication",
|
||||||
"model": "AI model to use",
|
"model": "AI model to use",
|
||||||
"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-100000 tokens)",
|
||||||
"api_endpoint": "Custom API endpoint URL (optional)",
|
"api_endpoint": "Custom API endpoint URL (optional)",
|
||||||
"api_provider": "API Provider",
|
"api_provider": "API Provider",
|
||||||
"request_interval": "Minimum time between requests (0.1-60 seconds)",
|
"request_interval": "Minimum time between requests (0.1-60 seconds)",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"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-100000)",
|
||||||
"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)"
|
"max_history_size": "Maximum conversation history size (1-100)"
|
||||||
@@ -86,17 +86,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"selector": {
|
"selector": {
|
||||||
"api_provider": {
|
"api_provider": {
|
||||||
"options": {
|
"options": {
|
||||||
"openai": "OpenAI (compatible)",
|
"openai": "OpenAI (compatible)",
|
||||||
"anthropic": "Anthropic (compatible)"
|
"anthropic": "Anthropic (compatible)",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"gemini": "Google Gemini"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Ask Question (HA Text AI)",
|
"name": "Ask Question (HA Text AI)",
|
||||||
"description": "Send a question to the AI model and receive a detailed response. The response will be stored in the conversation history and can be retrieved later.",
|
"description": "Send a question to the AI model and receive a detailed response. This service now returns response data directly, eliminating the need for separate text sensors and the 255-character limitation. The response will also be stored in the conversation history.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Instance",
|
"name": "Instance",
|
||||||
@@ -124,7 +126,7 @@
|
|||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Max Tokens",
|
"name": "Max Tokens",
|
||||||
"description": "Maximum length of the response (1-4096 tokens)"
|
"description": "Maximum length of the response (1-100000 tokens)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"model": "Modelo de IA a utilizar",
|
"model": "Modelo de IA a utilizar",
|
||||||
"api_endpoint": "URL del endpoint de API personalizado (opcional)",
|
"api_endpoint": "URL del endpoint de API personalizado (opcional)",
|
||||||
"temperature": "Creatividad de la respuesta (0-2, menor = más enfocado)",
|
"temperature": "Creatividad de la respuesta (0-2, menor = más enfocado)",
|
||||||
"max_tokens": "Longitud máxima de la respuesta (1-4096 tokens)",
|
"max_tokens": "Longitud máxima de la respuesta (1-100000 tokens)",
|
||||||
"request_interval": "Tiempo mínimo entre solicitudes (0.1-60 segundos)",
|
"request_interval": "Tiempo mínimo entre solicitudes (0.1-60 segundos)",
|
||||||
"context_messages": "Número de mensajes de contexto a retener (1-20)",
|
"context_messages": "Número de mensajes de contexto a retener (1-20)",
|
||||||
"max_history_size": "Tamaño máximo del historial de conversación (1-100)"
|
"max_history_size": "Tamaño máximo del historial de conversación (1-100)"
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"api_key": "Clave API para autenticación",
|
"api_key": "Clave API para autenticación",
|
||||||
"model": "Modelo de IA a utilizar",
|
"model": "Modelo de IA a utilizar",
|
||||||
"temperature": "Creatividad de la respuesta (0-2, menor = más enfocado)",
|
"temperature": "Creatividad de la respuesta (0-2, menor = más enfocado)",
|
||||||
"max_tokens": "Longitud máxima de la respuesta (1-4096 tokens)",
|
"max_tokens": "Longitud máxima de la respuesta (1-100000 tokens)",
|
||||||
"api_endpoint": "URL del endpoint de API personalizado (opcional)",
|
"api_endpoint": "URL del endpoint de API personalizado (opcional)",
|
||||||
"api_provider": "Proveedor de API",
|
"api_provider": "Proveedor de API",
|
||||||
"request_interval": "Tiempo mínimo entre solicitudes (0.1-60 segundos)",
|
"request_interval": "Tiempo mínimo entre solicitudes (0.1-60 segundos)",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"model": "Modelo de IA",
|
"model": "Modelo de IA",
|
||||||
"temperature": "Creatividad de la respuesta (0-2)",
|
"temperature": "Creatividad de la respuesta (0-2)",
|
||||||
"max_tokens": "Longitud máxima de la respuesta (1-4096)",
|
"max_tokens": "Longitud máxima de la respuesta (1-100000)",
|
||||||
"request_interval": "Intervalo mínimo de solicitud (0.1-60 segundos)",
|
"request_interval": "Intervalo mínimo de solicitud (0.1-60 segundos)",
|
||||||
"context_messages": "Número de mensajes anteriores a incluir en el contexto (1-20)",
|
"context_messages": "Número de mensajes anteriores a incluir en el contexto (1-20)",
|
||||||
"max_history_size": "Tamaño máximo del historial de conversación (1-100)"
|
"max_history_size": "Tamaño máximo del historial de conversación (1-100)"
|
||||||
@@ -89,14 +89,16 @@
|
|||||||
"api_provider": {
|
"api_provider": {
|
||||||
"options": {
|
"options": {
|
||||||
"openai": "OpenAI (compatible)",
|
"openai": "OpenAI (compatible)",
|
||||||
"anthropic": "Anthropic (compatible)"
|
"anthropic": "Anthropic (compatible)",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"gemini": "Google Gemini"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Hacer Pregunta (HA Text AI)",
|
"name": "Hacer Pregunta (HA Text AI)",
|
||||||
"description": "Envía una pregunta al modelo de IA y recibe una respuesta detallada. La respuesta se almacenará en el historial de conversación y se podrá recuperar más tarde.",
|
"description": "Envía una pregunta al modelo de IA y recibe una respuesta detallada. Este servicio ahora devuelve datos de respuesta directamente, eliminando la necesidad de sensores de texto separados y la limitación de 255 caracteres. La respuesta también se almacenará en el historial de conversación.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Instancia",
|
"name": "Instancia",
|
||||||
@@ -124,7 +126,7 @@
|
|||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Máx. Tokens",
|
"name": "Máx. Tokens",
|
||||||
"description": "Longitud máxima de la respuesta (1-4096 tokens)"
|
"description": "Longitud máxima de la respuesta (1-100000 tokens)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"provider": {
|
|
||||||
"title": "एआई प्रदाता चुनें",
|
|
||||||
"description": "इस उदाहरण के लिए किस एआई सेवा प्रदाता का उपयोग करना है, चुनें।",
|
|
||||||
"data": {
|
|
||||||
"api_provider": "एपीआई प्रदाता",
|
|
||||||
"context_messages": "रखने के लिए संदर्भ संदेशों की संख्या (1-20)",
|
|
||||||
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"provider": {
|
"provider": {
|
||||||
"title": "प्रदाता सेटिंग्स",
|
"title": "प्रदाता सेटिंग्स",
|
||||||
"description": "आपके द्वारा चुने गए एआई प्रदाता के लिए कनेक्शन विवरण प्रदान करें।",
|
"description": "आपके द्वारा चुने गए एआई प्रदाता के लिए कनेक्शन विवरण प्रदान करें।",
|
||||||
@@ -19,7 +10,7 @@
|
|||||||
"model": "उपयोग करने के लिए एआई मॉडल",
|
"model": "उपयोग करने के लिए एआई मॉडल",
|
||||||
"api_endpoint": "कस्टम एपीआई एंडपॉइंट यूआरएल (वैकल्पिक)",
|
"api_endpoint": "कस्टम एपीआई एंडपॉइंट यूआरएल (वैकल्पिक)",
|
||||||
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2, कम = अधिक केंद्रित)",
|
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2, कम = अधिक केंद्रित)",
|
||||||
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-4096 टोकन)",
|
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-100000 टोकन)",
|
||||||
"request_interval": "अनुरोधों के बीच न्यूनतम समय (0.1-60 सेकंड)",
|
"request_interval": "अनुरोधों के बीच न्यूनतम समय (0.1-60 सेकंड)",
|
||||||
"context_messages": "रखने के लिए संदर्भ संदेशों की संख्या (1-20)",
|
"context_messages": "रखने के लिए संदर्भ संदेशों की संख्या (1-20)",
|
||||||
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
|
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
|
||||||
@@ -33,7 +24,7 @@
|
|||||||
"api_key": "प्रमाणीकरण के लिए एपीआई कुंजी",
|
"api_key": "प्रमाणीकरण के लिए एपीआई कुंजी",
|
||||||
"model": "उपयोग करने के लिए एआई मॉडल",
|
"model": "उपयोग करने के लिए एआई मॉडल",
|
||||||
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2, कम = अधिक केंद्रित)",
|
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2, कम = अधिक केंद्रित)",
|
||||||
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-4096 टोकन)",
|
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-100000 टोकन)",
|
||||||
"api_endpoint": "कस्टम एपीआई एंडपॉइंट यूआरएल (वैकल्पिक)",
|
"api_endpoint": "कस्टम एपीआई एंडपॉइंट यूआरएल (वैकल्पिक)",
|
||||||
"api_provider": "एपीआई प्रदाता",
|
"api_provider": "एपीआई प्रदाता",
|
||||||
"request_interval": "अनुरोधों के बीच न्यूनतम समय (0.1-60 सेकंड)",
|
"request_interval": "अनुरोधों के बीच न्यूनतम समय (0.1-60 सेकंड)",
|
||||||
@@ -77,7 +68,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"model": "एआई मॉडल",
|
"model": "एआई मॉडल",
|
||||||
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2)",
|
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2)",
|
||||||
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-4096)",
|
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-100000)",
|
||||||
"request_interval": "न्यूनतम अनुरोध अंतराल (0.1-60 सेकंड)",
|
"request_interval": "न्यूनतम अनुरोध अंतराल (0.1-60 सेकंड)",
|
||||||
"context_messages": "संदर्भ में शामिल करने के लिए पिछले संदेशों की संख्या (1-20)",
|
"context_messages": "संदर्भ में शामिल करने के लिए पिछले संदेशों की संख्या (1-20)",
|
||||||
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
|
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
|
||||||
@@ -89,14 +80,16 @@
|
|||||||
"api_provider": {
|
"api_provider": {
|
||||||
"options": {
|
"options": {
|
||||||
"openai": "OpenAI (अनुकूलित)",
|
"openai": "OpenAI (अनुकूलित)",
|
||||||
"anthropic": "Anthropic (अनुकूलित)"
|
"anthropic": "Anthropic (अनुकूलित)",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"gemini": "Google Gemini"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "प्रश्न पूछें (HA Text AI)",
|
"name": "प्रश्न पूछें (HA Text AI)",
|
||||||
"description": "एआई मॉडल को एक प्रश्न भेजें और विस्तृत प्रतिक्रिया प्राप्त करें। प्रतिक्रिया बातचीत के इतिहास में संग्रहीत की जाएगी और बाद में पुनर्प्राप्त की जा सकती है।",
|
"description": "AI मॉडल को प्रश्न भेजें और विस्तृत उत्तर प्राप्त करें। यह सेवा अब प्रत्यक्ष रूप से प्रतिक्रिया डेटा वापस करती है, अलग टेक्स्ट सेंसर की आवश्यकता और 255 वर्ण की सीमा को समाप्त करती है। प्रतिक्रिया को बातचीत के इतिहास में भी संग्रहीत किया जाएगा।",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "उदाहरण",
|
"name": "उदाहरण",
|
||||||
@@ -124,7 +117,7 @@
|
|||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "अधिकतम टोकन",
|
"name": "अधिकतम टोकन",
|
||||||
"description": "प्रतिक्रिया की अधिकतम लंबाई (1-4096 टोकन)"
|
"description": "प्रतिक्रिया की अधिकतम लंबाई (1-100000 टोकन)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"model": "Modello AI da utilizzare",
|
"model": "Modello AI da utilizzare",
|
||||||
"api_endpoint": "URL dell'endpoint API personalizzato (opzionale)",
|
"api_endpoint": "URL dell'endpoint API personalizzato (opzionale)",
|
||||||
"temperature": "Creatività della risposta (0-2, più basso = più focalizzato)",
|
"temperature": "Creatività della risposta (0-2, più basso = più focalizzato)",
|
||||||
"max_tokens": "Lunghezza massima della risposta (1-4096 token)",
|
"max_tokens": "Lunghezza massima della risposta (1-100000 token)",
|
||||||
"request_interval": "Tempo minimo tra le richieste (0.1-60 secondi)",
|
"request_interval": "Tempo minimo tra le richieste (0.1-60 secondi)",
|
||||||
"context_messages": "Numero di messaggi di contesto da mantenere (1-20)",
|
"context_messages": "Numero di messaggi di contesto da mantenere (1-20)",
|
||||||
"max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)"
|
"max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)"
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"api_key": "Chiave API per l'autenticazione",
|
"api_key": "Chiave API per l'autenticazione",
|
||||||
"model": "Modello AI da utilizzare",
|
"model": "Modello AI da utilizzare",
|
||||||
"temperature": "Creatività della risposta (0-2, più basso = più focalizzato)",
|
"temperature": "Creatività della risposta (0-2, più basso = più focalizzato)",
|
||||||
"max_tokens": "Lunghezza massima della risposta (1-4096 token)",
|
"max_tokens": "Lunghezza massima della risposta (1-100000 token)",
|
||||||
"api_endpoint": "URL dell'endpoint API personalizzato (opzionale)",
|
"api_endpoint": "URL dell'endpoint API personalizzato (opzionale)",
|
||||||
"api_provider": "Fornitore API",
|
"api_provider": "Fornitore API",
|
||||||
"request_interval": "Tempo minimo tra le richieste (0.1-60 secondi)",
|
"request_interval": "Tempo minimo tra le richieste (0.1-60 secondi)",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"model": "Modello AI",
|
"model": "Modello AI",
|
||||||
"temperature": "Creatività della risposta (0-2)",
|
"temperature": "Creatività della risposta (0-2)",
|
||||||
"max_tokens": "Lunghezza massima della risposta (1-4096)",
|
"max_tokens": "Lunghezza massima della risposta (1-100000)",
|
||||||
"request_interval": "Intervallo minimo di richiesta (0.1-60 secondi)",
|
"request_interval": "Intervallo minimo di richiesta (0.1-60 secondi)",
|
||||||
"context_messages": "Numero di messaggi precedenti da includere nel contesto (1-20)",
|
"context_messages": "Numero di messaggi precedenti da includere nel contesto (1-20)",
|
||||||
"max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)"
|
"max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)"
|
||||||
@@ -89,14 +89,16 @@
|
|||||||
"api_provider": {
|
"api_provider": {
|
||||||
"options": {
|
"options": {
|
||||||
"openai": "OpenAI (compatibile)",
|
"openai": "OpenAI (compatibile)",
|
||||||
"anthropic": "Anthropic (compatibile)"
|
"anthropic": "Anthropic (compatibile)",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"gemini": "Google Gemini"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Fai una domanda (HA Text AI)",
|
"name": "Fai una domanda (HA Text AI)",
|
||||||
"description": "Invia una domanda al modello AI e ricevi una risposta dettagliata. La risposta sarà memorizzata nella cronologia delle conversazioni e potrà essere recuperata in seguito.",
|
"description": "Invia una domanda al modello AI e ricevi una risposta dettagliata. Questo servizio ora restituisce i dati di risposta direttamente, eliminando la necessità di sensori di testo separati e la limitazione di 255 caratteri. La risposta sarà anche memorizzata nella cronologia delle conversazioni.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Istanze",
|
"name": "Istanze",
|
||||||
@@ -124,7 +126,7 @@
|
|||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Token massimi",
|
"name": "Token massimi",
|
||||||
"description": "Lunghezza massima della risposta (1-4096 token)"
|
"description": "Lunghezza massima della risposta (1-100000 token)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"model": "Модель ИИ для использования",
|
"model": "Модель ИИ для использования",
|
||||||
"api_endpoint": "Пользовательский URL-адрес конечной точки API (необязательно)",
|
"api_endpoint": "Пользовательский URL-адрес конечной точки API (необязательно)",
|
||||||
"temperature": "Креативность ответа (0-2, меньше = более сфокусированно)",
|
"temperature": "Креативность ответа (0-2, меньше = более сфокусированно)",
|
||||||
"max_tokens": "Максимальная длина ответа (1-4096 токенов)",
|
"max_tokens": "Максимальная длина ответа (1-100000 токенов)",
|
||||||
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
||||||
"context_messages": "Количество сохраняемых контекстных сообщений (1-20)",
|
"context_messages": "Количество сохраняемых контекстных сообщений (1-20)",
|
||||||
"max_history_size": "Максимальный размер истории разговора (1-100)"
|
"max_history_size": "Максимальный размер истории разговора (1-100)"
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"api_key": "API-ключ для аутентификации",
|
"api_key": "API-ключ для аутентификации",
|
||||||
"model": "Модель ИИ для использования",
|
"model": "Модель ИИ для использования",
|
||||||
"temperature": "Креативность ответа (0-2, меньше = более сфокусированно)",
|
"temperature": "Креативность ответа (0-2, меньше = более сфокусированно)",
|
||||||
"max_tokens": "Максимальная длина ответа (1-4096 токенов)",
|
"max_tokens": "Максимальная длина ответа (1-100000 токенов)",
|
||||||
"api_endpoint": "Пользовательский URL-адрес конечной точки API (необязательно)",
|
"api_endpoint": "Пользовательский URL-адрес конечной точки API (необязательно)",
|
||||||
"api_provider": "Провайдер API",
|
"api_provider": "Провайдер API",
|
||||||
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"model": "Модель ИИ",
|
"model": "Модель ИИ",
|
||||||
"temperature": "Креативность ответа (0-2)",
|
"temperature": "Креативность ответа (0-2)",
|
||||||
"max_tokens": "Максимальная длина ответа (1-4096)",
|
"max_tokens": "Максимальная длина ответа (1-100000)",
|
||||||
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
||||||
"context_messages": "Количество предыдущих сообщений для включения в контекст (1-20)",
|
"context_messages": "Количество предыдущих сообщений для включения в контекст (1-20)",
|
||||||
"max_history_size": "Максимальный размер истории разговора (1-100)"
|
"max_history_size": "Максимальный размер истории разговора (1-100)"
|
||||||
@@ -89,14 +89,16 @@
|
|||||||
"api_provider": {
|
"api_provider": {
|
||||||
"options": {
|
"options": {
|
||||||
"openai": "OpenAI (совместимый)",
|
"openai": "OpenAI (совместимый)",
|
||||||
"anthropic": "Anthropic (совместимый)"
|
"anthropic": "Anthropic (совместимый)",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"gemini": "Google Gemini"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Задать вопрос (HA Text AI)",
|
"name": "Задать вопрос (HA Text AI)",
|
||||||
"description": "Отправить вопрос модели ИИ и получить подробный ответ. Ответ будет сохранен в истории разговора и может быть получен позже.",
|
"description": "Отправить вопрос модели ИИ и получить подробный ответ. Сервис теперь возвращает данные ответа напрямую, устраняя необходимость в отдельных текстовых сенсорах и ограничение в 255 символов. Ответ также будет сохранен в истории разговора.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Экземпляр",
|
"name": "Экземпляр",
|
||||||
@@ -124,7 +126,7 @@
|
|||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Максимум токенов",
|
"name": "Максимум токенов",
|
||||||
"description": "Максимальная длина ответа (1-4096 токенов)"
|
"description": "Максимальная длина ответа (1-100000 токенов)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"provider": {
|
|
||||||
"title": "Изаберите AI провајдера",
|
|
||||||
"description": "Изаберите који AI сервис провајдер да користите за ову инстанцу.",
|
|
||||||
"data": {
|
|
||||||
"api_provider": "API провајдер",
|
|
||||||
"context_messages": "Број контекстуалних порука које треба задржати (1-20)",
|
|
||||||
"max_history_size": "Максимална величина историје разговора (1-100)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"provider": {
|
"provider": {
|
||||||
"title": "Подешавања провајдера",
|
"title": "Подешавања провајдера",
|
||||||
"description": "Обезбедите детаље о вези за изабраног AI провајдера.",
|
"description": "Обезбедите детаље о вези за изабраног AI провајдера.",
|
||||||
@@ -19,7 +10,7 @@
|
|||||||
"model": "AI модел који ће се користити",
|
"model": "AI модел који ће се користити",
|
||||||
"api_endpoint": "Прилагођени URL API крајње тачке (опционо)",
|
"api_endpoint": "Прилагођени URL API крајње тачке (опционо)",
|
||||||
"temperature": "Креативност одговора (0-2, нижа = фокусираније)",
|
"temperature": "Креативност одговора (0-2, нижа = фокусираније)",
|
||||||
"max_tokens": "Максимална дужина одговора (1-4096 токена)",
|
"max_tokens": "Максимална дужина одговора (1-100000 токена)",
|
||||||
"request_interval": "Минимално време између захтева (0.1-60 секунди)",
|
"request_interval": "Минимално време између захтева (0.1-60 секунди)",
|
||||||
"context_messages": "Број контекстуалних порука које треба задржати (1-20)",
|
"context_messages": "Број контекстуалних порука које треба задржати (1-20)",
|
||||||
"max_history_size": "Максимална величина историје разговора (1-100)"
|
"max_history_size": "Максимална величина историје разговора (1-100)"
|
||||||
@@ -33,7 +24,7 @@
|
|||||||
"api_key": "API кључ за аутентификацију",
|
"api_key": "API кључ за аутентификацију",
|
||||||
"model": "AI модел који ће се користити",
|
"model": "AI модел који ће се користити",
|
||||||
"temperature": "Креативност одговора (0-2, нижа = фокусираније)",
|
"temperature": "Креативност одговора (0-2, нижа = фокусираније)",
|
||||||
"max_tokens": "Максимална дужина одговора (1-4096 токена)",
|
"max_tokens": "Максимална дужина одговора (1-100000 токена)",
|
||||||
"api_endpoint": "Прилагођени URL API крајње тачке (опционо)",
|
"api_endpoint": "Прилагођени URL API крајње тачке (опционо)",
|
||||||
"api_provider": "API провајдер",
|
"api_provider": "API провајдер",
|
||||||
"request_interval": "Минимално време између захтева (0.1-60 секунди)",
|
"request_interval": "Минимално време између захтева (0.1-60 секунди)",
|
||||||
@@ -77,7 +68,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"model": "AI модел",
|
"model": "AI модел",
|
||||||
"temperature": "Креативност одговора (0-2)",
|
"temperature": "Креативност одговора (0-2)",
|
||||||
"max_tokens": "Максимална дужина одговора (1-4096)",
|
"max_tokens": "Максимална дужина одговора (1-100000)",
|
||||||
"request_interval": "Минимално време између захтева (0.1-60 секунди)",
|
"request_interval": "Минимално време између захтева (0.1-60 секунди)",
|
||||||
"context_messages": "Број претходних порука које треба укључити у контекст (1-20)",
|
"context_messages": "Број претходних порука које треба укључити у контекст (1-20)",
|
||||||
"max_history_size": "Максимална величина историје разговора (1-100)"
|
"max_history_size": "Максимална величина историје разговора (1-100)"
|
||||||
@@ -89,14 +80,16 @@
|
|||||||
"api_provider": {
|
"api_provider": {
|
||||||
"options": {
|
"options": {
|
||||||
"openai": "OpenAI (компатибилан)",
|
"openai": "OpenAI (компатибилан)",
|
||||||
"anthropic": "Anthropic (компатибилан)"
|
"anthropic": "Anthropic (компатибилан)",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"gemini": "Google Gemini"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Поставите питање (HA Text AI)",
|
"name": "Поставите питање (HA Text AI)",
|
||||||
"description": "Пошаљите питање AI моделу и примите детаљан одговор. Одговор ће бити сачуван у историји разговора и може се касније повратити.",
|
"description": "Пошаљите питање AI моделу и добијте детаљан одговор. Овај сервис сада враћа податке одговора директно, елиминишући потребу за засебним текстуалним сензорима и ограничење од 255 карактера. Одговор ће такође бити сачуван у историји разговора.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Инстанца",
|
"name": "Инстанца",
|
||||||
@@ -124,7 +117,7 @@
|
|||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Максимални токени",
|
"name": "Максимални токени",
|
||||||
"description": "Максимална дужина одговора (1-4096 токена)"
|
"description": "Максимална дужина одговора (1-100000 токена)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"provider": {
|
|
||||||
"title": "选择AI提供者",
|
|
||||||
"description": "选择要用于此实例的AI服务提供者。",
|
|
||||||
"data": {
|
|
||||||
"api_provider": "API提供者",
|
|
||||||
"context_messages": "保留的上下文消息数量(1-20)",
|
|
||||||
"max_history_size": "最大对话历史大小(1-100)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"provider": {
|
"provider": {
|
||||||
"title": "提供者设置",
|
"title": "提供者设置",
|
||||||
"description": "提供所选AI提供者的连接详细信息。",
|
"description": "提供所选AI提供者的连接详细信息。",
|
||||||
@@ -19,7 +10,7 @@
|
|||||||
"model": "要使用的AI模型",
|
"model": "要使用的AI模型",
|
||||||
"api_endpoint": "自定义API端点URL(可选)",
|
"api_endpoint": "自定义API端点URL(可选)",
|
||||||
"temperature": "响应创造力(0-2,越低越专注)",
|
"temperature": "响应创造力(0-2,越低越专注)",
|
||||||
"max_tokens": "最大响应长度(1-4096个标记)",
|
"max_tokens": "最大响应长度(1-100000个标记)",
|
||||||
"request_interval": "请求之间的最小时间(0.1-60秒)",
|
"request_interval": "请求之间的最小时间(0.1-60秒)",
|
||||||
"context_messages": "保留的上下文消息数量(1-20)",
|
"context_messages": "保留的上下文消息数量(1-20)",
|
||||||
"max_history_size": "最大对话历史大小(1-100)"
|
"max_history_size": "最大对话历史大小(1-100)"
|
||||||
@@ -33,7 +24,7 @@
|
|||||||
"api_key": "用于身份验证的API密钥",
|
"api_key": "用于身份验证的API密钥",
|
||||||
"model": "要使用的AI模型",
|
"model": "要使用的AI模型",
|
||||||
"temperature": "响应创造力(0-2,越低越专注)",
|
"temperature": "响应创造力(0-2,越低越专注)",
|
||||||
"max_tokens": "最大响应长度(1-4096个标记)",
|
"max_tokens": "最大响应长度(1-100000个标记)",
|
||||||
"api_endpoint": "自定义API端点URL(可选)",
|
"api_endpoint": "自定义API端点URL(可选)",
|
||||||
"api_provider": "API提供者",
|
"api_provider": "API提供者",
|
||||||
"request_interval": "请求之间的最小时间(0.1-60秒)",
|
"request_interval": "请求之间的最小时间(0.1-60秒)",
|
||||||
@@ -77,7 +68,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"model": "AI模型",
|
"model": "AI模型",
|
||||||
"temperature": "响应创造力(0-2)",
|
"temperature": "响应创造力(0-2)",
|
||||||
"max_tokens": "最大响应长度(1-4096)",
|
"max_tokens": "最大响应长度(1-100000)",
|
||||||
"request_interval": "最小请求间隔(0.1-60秒)",
|
"request_interval": "最小请求间隔(0.1-60秒)",
|
||||||
"context_messages": "要包含在上下文中的先前消息数量(1-20)",
|
"context_messages": "要包含在上下文中的先前消息数量(1-20)",
|
||||||
"max_history_size": "最大对话历史大小(1-100)"
|
"max_history_size": "最大对话历史大小(1-100)"
|
||||||
@@ -89,14 +80,16 @@
|
|||||||
"api_provider": {
|
"api_provider": {
|
||||||
"options": {
|
"options": {
|
||||||
"openai": "OpenAI(兼容)",
|
"openai": "OpenAI(兼容)",
|
||||||
"anthropic": "Anthropic(兼容)"
|
"anthropic": "Anthropic(兼容)",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"gemini": "Google Gemini"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "提问 (HA Text AI)",
|
"name": "提问 (HA Text AI)",
|
||||||
"description": "向AI模型发送问题并接收详细响应。响应将存储在对话历史中,可以稍后检索。",
|
"description": "向AI模型发送问题并获得详细回答。此服务现在直接返回响应数据,消除了对单独文本传感器的需要和255字符限制。响应也将存储在对话历史中。",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "实例",
|
"name": "实例",
|
||||||
@@ -124,7 +117,7 @@
|
|||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "最大标记数",
|
"name": "最大标记数",
|
||||||
"description": "响应的最大长度(1-4096个标记)"
|
"description": "响应的最大长度(1-100000个标记)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user